UnSQL

UnSQL is a lightweight, open-source JavaScript library that facilitates class based, schemaless interactions with the structured databases viz. MySQL
, PostgreSQL
and SQLite
through dynamic query generation. It is the only library that supports single codebase across all dialects. It is compatible with javascript runtime environments like NodeJS and NextJS.
Table of Contents
1. Overview
UnSQL simplifies working with structured databases by dynamically generating SQLs under the hood. It provides developer friendly interface while eliminating the complexities of SQL. UnSQL also utilizes placeholders and parameterized SQL statements to prevent SQL-injections.
1.1 Breaking Changes
With the release of version v2.0, UnSQL has been re-written from scratch to cater modern challenges, including enhanced security and including new features all while also keeping the interface clean and simple, improving the overall developer experience. If your project is still using version v1.x then it is recommended you switch your import/require
from 'unsql'
to 'unsql/legacy'
, as shown below:
const UnSQL = require('unsql/legacy')
import UnSQL from 'unsql/legacy'
const { UnSQL } = require('unsql')
import { UnSQL } from 'unsql'
Documentation for v1.x can be found on GitHub
1.2 What's New?
Version v2.1 brings support for Multiple Dialects along with Unified codebase, Bug Fixes, Improved Code Suggestions, brought back the rawQuery Method, enhanced Session Manager and better code optimization under the hood and much more
1.3 Key Features
- Promise based interface with streamlined async/await support
- Schemaless eliminates boilerplate code and hectic to manage migrations
- Unified Codebase enables maintaining single codebase while switching between SQL dialects
- Class-based Models encapsulates configurations into clean interface
- Reuse connections supports connection
pool
for better performance
- Dynamic query generation perform CRUDs without writing SQL
- Safer code prevents SQL-injections with placeholders and prepared statements
- JSON as Response including execution success/failure acknowledgement and
result
or error
- Transaction based executions, handles rollbacks on failure
- Graceful Error Handling no try-catch required, returns structured error message
- JSDoc-compatible for type checking and code suggestions
- Built-in Debug Modes (eg.: 'query', 'error', 'benchmarks' etc)
- Built-in AES Encryption/Decryption protect sensitive data natively without any third part package
2. Getting Started
2.1 Prerequisites
UnSQL can work with three different dialect
of SQL ('mysql'
, 'postgresql'
and 'sqlite'
). Each of them require different prerequisite setup which are utilized by UnSQL as a source of connection pool
as mentioned below:
import mysql2 from 'mysql2/promise'
export const pool = createPool({
host: 'localhost',
database: 'test_db',
user: 'your_username',
password: 'your_password',
namedPlaceholders: true,
multipleStatements: true
})
import { Pool } from 'pg'
export const pool = new Pool({
host: 'localhost',
database: 'test_db',
user: 'your_username',
password: 'your_password'
})
import sqlite3 from 'sqlite3'
import { open } from 'sqlite'
export const pool = (async () => {
try {
return await open({
filename: './databases/test2.db',
driver: sqlite3.Database
})
} catch (error) {
console.error('Error initializing database:', error)
throw error
}
})()
Please note:
- Named placeholders and multiline statement settings are only required to be configured with MySQL
- Although SQLite provides connection reference (here
db
), it is still used with pool
property of config
2.2 Installation
UnSQL can be installed using any of the package managers viz. npm
or yarn
:
npm i unsql
yarn add unsql
2.3 Setup Guide
Unsql uses class based approach hence, after prerequisites and installation, next step is to create model classes. Each model is mapped to a database table and extends from the UnSQL base class and has a static property named config
that holds all the configurations related to the respective model class. Below if the sample model class using CommonJS and ES6 Module:
const { UnSQL } = require('unsql')
const pool = require('path/to/your/db/service')
class User extends UnSQL {
static config = {
table: 'test_user',
pool,
safeMode: true,
devMode: false,
dialect: 'mysql'
}
}
module.exports = { User }
- user.class.js (ES6 Module)
import { UnSQL } from 'unsql'
import { pool } from 'path/to/your/db/service'
export class User extends UnSQL {
static config = {
table: 'test_user',
pool,
safeMode: true,
devMode: false,
dialect: 'mysql'
}
}
2.3.1 Config Property
Config
property is the heart and soul of any model class, it holds all configurations related to the model class and is used throughout query generation and execution. It can also hold global level configurations related to Encryption/Decryption for that table so that you don't have to re-define them for each query.
table | (required) name of the database table to be mapped with this model class |
pool | (required) connection / pool of connection provided by prerequisite package |
safeMode | (required) defaults to true prevents accidental delete all and reset query |
devMode | (required) defaults to false , unless true , prevents export/import of data |
dialect | (required) defines the dialect used for dynamic query generation |
encryption | (optional) defines various properties viz. secret , iv , sha and mode at global level to used by all executions |
dbEncryptionMode | (optional) defaults to unknown , defines the encryption mode set on the database |
Please note:
secret
is the secret key that is used to encrypt the data
iv
and sha
are only used when dialect
is set to 'mysql'
, as postgresql
sets up iv
internally and sqlite
does not have any built-in Encryption/Decryption methods
- When
dbEncryptionMode
is same as mode
inside encryption
property, in mysql
dialect, an additional internal query that is used to set the block_encryption_mode
is skipped
3. Built-in Query Methods
Unsql provides various static, asynchronous built-in methods as mentioned below:
find | used to read / retrieve /fetch record(s) from database |
save | used to insert / update / upsert record(s) into database |
delete | used to remove / delete record(s) from database |
rawQuery | used to write custom SQL (manually), can be used for any of type of query execution |
reset | will remove all record(s) and reset auto increment column to initial state |
export | can dump record(s) from database to specified target (json file or model class) |
Each of these methods are explained below:
3.1 Find Method
find
is a static, asynchronous method used to fetch record(s) from the database or add a dummy column(s) with static value(s) while execution. It can also perform several operations like re-order, filter, mutate or even Encryption/Decryption of record(s) while fetching. It can also combine multiple tables as child associations and retrieve record(s) from these tables combined. UnSQL has combined the features of findOne and findAll methods into one find
method, as findOne (in other libraries) is just a wrapper around findAll to fetch first returning record irrespective of the response set. Interface of find
method along with its default properties is explained below:
const response = await User.find({
alias: undefined,
select: ['*'],
join: [],
where: {},
junction: 'and',
groupBy: [],
having: {},
orderBy: {},
limit: undefined,
offset: undefined,
encryption: {},
debug: false,
session: undefined
})
Each of these properties is explained below:
-
alias
provides local reference name to the table. It is context sensitive hence when alias are defined in nested objects, each alias is by default attached to all columns inside that context, to use a different alias (from parent or child table), reference to that alias must be prefixed to that column name along with '.'
symbol in between
-
select
is an array of values, each value can be column name, static string/number/boolean value, or any of the reserved keyword(s) or wrapper object(s). It is used to restrict the column(s) to be fetched from the database or create dummy column(s), or mutate any value (through wrapper object(s)) at execution
-
join
is an array of objects where each object represents association of a child table with this parent (model class) table. Below is the interface for join object, similar to find
:
{
type: '',
alias: undefined,
table: null,
select: ['*'],
join: [],
where: {},
junction: 'and',
groupBy: [],
having: {},
orderBy: {},
limit: undefined,
offset: undefined,
using: [],
as: null
}
const response = await Order.find({
select: ['orderId', 'createdOn',
{
json: {
value: {
itemId: 'itemId',
name: 'itemName',
quantity: 'quantity'
},
aggregate: true,
as: 'items'
}
}
],
join: [{ table: 'order_items', using: ['orderId'] }]
})
Please note:
using
property can accept array of column names or an object like { parentColumn: childColumn }
where parentColumn
is the column from parent table and childColumn
is the column from child table. When alias
is passed, it is automatically patched to the respective column name
- When using
select
| where
| having
inside join, as
is mandatory
- When both
alias
and as
is set, as
will be used as prefix to refer column names from child tables outside join object context
Below is the explanation for each of these join types:
natural
based on columns with the same name and datatype (automatically detected)
left
considers all records in parent table and only matching records from child table
right
considers all records in child table and only matching records from parent table (not supported by sqlite
)
inner
only matching rows based on using
column(s)
cross
cartesian product of records in parent and child tables
fullOuter
returns all records from both tables, regardless of matching condition (only supported by postgresql
)
-
where
filters record(s) to be fetched from the database based on the conditions provided as simple (or nested) objects in key: value
pairs, comparator methods, wrapper methods etc.
const response = await User.find({
where: {
department: ['#marketing', '#sales'],
joiningDate: { between: { gt: '2025-01-01', lt: 'now' } },
or: [{userStatus: 1}, {userStatus: 2}]
}
})
-
junction
determines the connecting clause ('and'
or 'or'
) that will be used to connect conditions provided inside where
and having
properties. Defaults to 'and'
-
groupBy
groups record(s) based on the column name(s) provided as an array
-
having
similar to where
, filter record(s) based on condition(s) the only difference is that it supports aggregate object(s) (in wrapper objects
)
-
orderBy
used to define the order in which record(s) are fetched
-
limit
limits the number of records to be fetched
-
offset
defines the starting index of the record(s) being fetched
-
encryption
defines configurations (similar to encryption
inside config
property) but limited to a specific execution (local level)
-
debug
enables various debug modes and prints to console: dynamically generated query (un-prepared and prepared statements), values to be injected, errors, benchmarks, based on the selected mode as explained below:
'query' | logs prepared, un-prepared, values |
'error' | logs entire error object in the console |
'benchmark' | logs out the time taken to execute the query |
benchmark-query' | enables combination of 'query' and 'benchmark' modes |
benchmark-error' | enables combination of 'error' and 'benchmark' modes |
true | enables all three modes i.e. 'query' , 'error' and 'benchmark' |
false | (default) disables all debug modes |
-
session
reference of SessionManager
object, used to override the transaction/commit/rollback features to be controlled externally
3.2 Save Method
save
is a static, asynchronous method, used to insert | update | upsert record(s) into the database. It can insert | update single or even multiple records (in bulk) in single execution. It also supports data Encryption during this process. When only data
property is set, this method operates in insert mode, when along with data
, any or both of where
and having
are also set, this method operates in update mode, and when along with data
, upsert
property is set, this method operates in upsert mode. Interface along with default values for this method is shown below:
const response = await User.save({
alias: undefined,
data,
where: {},
junction: 'and',
groupBy: [],
having: {},
upsert: {},
encrypt: {},
encryption: {},
debug: false,
session: undefined
})
Each of these properties is explained below:
alias
same as explained here
data
(required) this is the actual data that will be inserted | updated into the database. It can either be a single object (supports insert | update | upsert) or an array of objects (supports only insert)
where
same as explained here, used to filter record(s) to be updated
junction
same as explained here
groupBy
same as explained here
having
same as explained here, used to filter record(s) to be updated
encrypt
accepts key: value
pair, where key
can be column name and value
is another object that holds configurations like secret
, iv
and sha
that will be used to encrypt this column. When no properties are set i.e. value
is set as {}
, in such a case, configurations defined in encryption
property (local or global) is used. This property helps encrypting different columns with different secret
debug
same as explained here
encryption
same as explained here
session
same as explained here
Please Note: In Upsert mode, while mysql
and postgresql
will only update the columns provided in the upsert
object, with dialect: 'sqlite'
if any existing column value is ignored in the upsert
object, then that value will either be set to null
or predefined default value
will be assigned to that column, due to the native upsert behavior (INSERT OR REPLACE) of SQLite
3.3 Delete Method
delete
is a static, asynchronous method, used to remove record(s) from the database. where
and having
properties are used to filter record(s) that will be removed, if no conditions are provided in where
and (or) having
property, this method will remove all records in the database. safeMode
property (when set to true
) in the config
property of the model class helps prevent accidental delete all of the records. Interface for this method along with default values is shown below:
{
alias: undefined,
where: {},
junction: 'and',
groupBy: [],
having: {},
encryption: {},
debug: false,
session: undefined
}
const response = await User.delete({
where: {
joiningDate: {
between: {
gt: { date: { value: 'now', sub: '1Y' } },
lt: { date: { value: 'now', sub: '6M' } }
}
},
department: ['sales', 'marketing'],
userType: 'intern'
}
})
Each of these properties is explained below:
alias
same as explained here
where
same as explained here, used to filter record(s) to be removed
junction
same as explained here
having
same as explained here, used to filter record(s) to be removed
debug
same as explained here
encryption
same as explained here
session
same as explained here
3.4 Raw Query Method
rawQuery
method is the most powerful method among all, unlike other methods that are limited to the base mapping, this method is not tied to any particular table, but utilizes the connection pool to execute queries on that database itself. It is capable of executing any and all types of queries including DDL, DML etc (In sqlite
, set methodType: 'exec'
). It also supports execution of multiple SQL statements in one query. When multiple SELECT
statements are executed (not supported by sqlite
), result
contains nested array one for each SELECT
statement.
In mysql
, use multiQuery: true
to enable execution of multiple SQL statements in single query
For sqlite
, UnSQL supports various types of methods (as mentioned below) that can be set manually, each method has specific capabilities:
all | supports Session Manager and SELECT query returns record(s) as array |
run | supports Session Manager, INSERT and UPDATE query, returns insertId and changes |
exec | supports CREATE, DROP ALTER and similar query, returns nothing |
It supports normal as well as parameterized (with placeholders) queries:
- In
mysql
:
- Positional placeholders:
??
, ?
,
- Named placeholders:
:namedVariable
,
- user defined variables:
@userVariable
,
- In
postgresql
:
- Positional placeholder:
$1
, $2
, $3
...
- In
sqlite
:
- Positional placeholder:
?
,
- Named placeholders:
:namedVariable
or $namedVariable
or @namedVariable
,
- Indexed placeholder:
$1
, $2
, $3
... or ?1
, ?2
, ?3
...
const response = await User.rawQuery({
sql: `CREATE TABLE IF NOT EXISTS users (
userId INT(11) PRIMARY KEY AUTO_INCREMENT,
firstName VARCHAR(45) DEFAULT NULL,
lastName VARCHAR(45) DEFAULT NULL,
email VARCHAR(255) UNIQUE DEFAULT NOT NULL,
password VARCHAR(255) DEFAULT NOT NULL,
createdOn TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
lastUpdatedOn TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
status TINYINT(1) DEFAULT 1
);
CREATE TABLE IF NOT EXISTS order_history (
orderId INT(11) PRIMARY KEY AUTO_INCREMENT,
amount DECIMAL (10,2) DEFAULT 0.00,
coupon VARCHAR(45) DEFAULT NULL,
discount DECIMAL (10,2) DEFAULT 0.00,
createdOn TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
lastUpdatedOn TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
status TINYINT(1) DEFAULT 0
);`,
multiQuery: true
})
3.5 Export Method
export
is a static, asynchronous method that works when devMode: true
is set in config
, as it is used to export record(s) from the database table either to a .json file or to another model class, depending upon the value set in the target
property. Interface and default values of this method are shown below:
{
target: 'table_name',
directory: 'exports_unsql',
alias: undefined,
select: ['*'],
join: [],
where: {},
groupBy: [],
having: {},
orderBy: [],
limit: undefined,
offset: undefined,
mode: 'append',
encrypt: undefined,
encryption: undefined,
debug: false
}
const response = await User.export()
const response = await User.export({
select: ['firstName', 'lastName', 'email', 'password', 'department', 'salary'],
target: User2
})
Each of these properties are explained below:
target
plays an important role, as it determines if the records will be exported .to a json file or to another model class. It defaults to the table
name property inside config
of the respective model class
- When set to a string value, record(s) will be exported to a .json file with that exact name,
- When another model class reference is passed as value, record(s) are exported (inserted in) to that model class
directory
determines the name of the folder that will be created (dynamically) to store the dynamically created .json file
alias
same as explained here
select
restricts the column(s) to be exported, also used to mutate values while exporting them including Decryption etc. Same as explained here
join
used to associate another table to fetch record(s) together with this table while exporting. Same as explained here, used to filter record(s) to be removed
where
filter record(s) to be exported. Same as explained here, used to filter record(s) to be removed
junction
same as explained here
groupBy
same as explained here
having
filter record(s) to be exported. Same as explained here, used to filter record(s) to be removed
orderBy
same as explained here
limit
limits the number of record(s) to be exported, Same as explained here
offset
defines the starting index for the record(s) to be exported. Same as explained here
mode
(works when exporting to a json file) when the export is executed and the file already contains data, this property determines whether to override
or append
the contents to the file
encrypt
encrypts the columns mentioned as key in this object during export. Same as explained here
encryption
same as explained here
debug
same as explained here
3.6 Reset Method
reset
is a static, asynchronous method used to clear all record(s) in the model class and also reset the auto increment ID (if any) to their initial state. This only works when devMode: true
and safeMode: false
in config
. This only expects one property debug
in its parameter object. Interface is shown below:
const response = await User.reset({ debug: false })
4. Built-in Constants, Units, Wrapper Objects and Comparator Objects
UnSQL has various Constants (Reserved Keywords), Units (Date/Time), Wrapper Objects and Comparator Objects. Each of them are explained below:
4.1 Constants (Reserved Keywords)
UnSQL
supports various built-in constants (supported by SQL) as mentioned below:
currentDate | provides only current date in YYYY-MM-DD format |
currentTime | provides only current time in hh:mm:ss format |
now | provides both, current date and time in YYYY-MM-DD hh:mm:ss format, with configured timezone |
currentTimestamp | synonym for now |
localTimestamp | similar to now or timestamp but in reference to local timezone |
localTime | exactly same as localTimestamp |
utcTimestamp | provides currentTimestamp in UTC format |
pi | provides value of mathematical constant pi i.e. approx. 3.141593 |
isNull | provides SQL compatible IS NULL value |
isNotNull | provides SQL compatible IS NOT NULL value |
4.2 Units (Date/Time)
UnSQL supports various Date / Time Patterns and Units for all sql dialects since the units and the format varies for each, unsql provides a unified symbols that are standard for all:
Please note:
- Due to limited / difference in implementation in all three dialects, some of the keywords mentioned below are not supported by respective sql dialect:
- MySQL:
tz
, TZ
, q
- SQLite:
tz
, TZ
, q
fromPattern
property / feature is not supported by sqlite
- Aforementioned units are only for formatting / creating date from string pattern and not to be confused with the date units used for addition / subtraction date / time units.
Please note: You can use them in combination like 2d 5m 1M 10y
in add
| sub
4.3 Wrapper Objects
UnSQL provides various built-in special objects to perform various specialized actions. Following is the list of special objects:
str | string | perform string based operations |
num | numeric | perform mathematical operations |
date | date | perform date related operations |
and | junction | perform junction override inside where and having property |
or | junction | perform junction override inside where and having property |
if | conditional | checks condition and returns respective true or false value |
case | conditional | checks multiple conditions and return respective value |
sum | aggregate | calculates total from set of values |
avg | aggregate | calculates average from set of values |
count | aggregate | performs count operation on set of values |
min | aggregate | determines lowest value among the provided values |
max | aggregate | determines highest value among the provided values |
json | sub-query | performs json object/array related operations |
refer | sub-query | fetch a column from another table at the position it is invoked |
concat | merge | combines multiple values into one |
All objects are explained below:
-
String wrapper (Keyword str
):
Performs string/text based operation(s) on value
property. Interface with default properties is shown below:
{
str: {
value: 'some value / column containing text',
replace: {
target: null,
replaceWith: null
},
reverse: false,
textCase: null,
padding: {
left: {
length: null,
pattern: null
},
right: {
length: null,
pattern: null
}
},
substr: {
start: 0,
length: null
},
trim: false,
cast: null,
decrypt: null,
encoding: 'utf8mb4',
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
str: {
value: 'firstName',
textCase: 'upper'
}
},
{
str: {
value: 'userBio',
substr: {
start: 1,
length: 50
},
as: 'shortBio'
}
},
{
str: {
value: 'email',
decrypt: {
secret: 'mySecret',
iv: 'customIV'
}
}
}
],
encryption: {
mode: 'aes-256-cbc'
}
})
Please note:
- All properties are optional and can be used in any combination
- All operations are performed on
value
property
reverse
and padding
are not supported by sqlite
cast
can be any of the values:
- For
mysql
:
'char'
| 'nchar'
| 'date'
| 'dateTime'
| 'signed'
| 'unsigned'
| 'decimal'
| 'binary'
- For
postgresql
:
'integer'
| 'text'
| 'timestamp'
| 'numeric'
- For
sqlite
:
'integer'
| 'text'
| 'real'
| 'blob'
decrypt
is an important property that holds an object with following properties:
secret
is the secret key when provided here, will override all other secret properties defined (if any) on execution level encryption
property or global level encryption
(inside config
)
iv
(Initialization Vector) only used with mysql
. Should be same for Encryption/Decryption. postgresql
manages iv
internally and does not require to be entered manually.
sha
determines the hash algorithm to be used (defaults to 512
) only used by mysql
. postgresql
does not require this.
sqlite
does not support built-in AES Encryption/Decryption hence will throw error if values are set
encoding
(only used with mysql
) determines the character set to be used while decrypting data. It can be any character set supported by mysql
like: 'utf8mb4'
(default) | 'latin1'
| 'ascii'
| 'utf8'
| 'ucs2'
| 'utf16'
etc
compare
is similar to where
and having
, it compares value returned by this object to the condition specified in this object.
- In
replace
property, due to limitation of implementation by SQL, target
and replaceWith
properties are always expected to be static string and never a column name, hence adding a prefix of #
is not required for these properties
-
Numeric Wrapper (Keyword num
):
Performs Numerical/Mathematical operation(s) on value
property. Follows the rules of BODMAS when performing multiple operations. Interface with default properties is shown below:
{
num: {
value: 'some number/ column containing number',
decimal: null,
mod: null,
sub: 0,
add: 0,
multiplyBy: null,
divideBy: null,
power: null,
cast: null,
decrypt: null,
encoding: 'utf8mb4',
as: null,
compare: {}
}
}
const response = await Specs.find({
select: [
{
num: {
value: 'calories',
decimal: 2,
multiplyBy: 100,
divideBy: 'quantity',
as: 'unitCalories'
}
}
]
})
Please note:
- All properties are optional and can be used in any combination
- All operations are performed on
value
property
- See cast, decrypt, encoding, compare for respective details
-
Date Wrapper (Keyword date
):
Performs Date/Time operation(s) on value
property. Interface along with default properties is shown below:
{
date: {
value: 'column containing date' || date,
add: null,
sub: null,
fromPattern: null,
cast: null,
decrypt: null,
encoding: 'utf8mb4',
format: null,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
date: {
value: 'joiningDate',
add: '6M',
format: 'null',
as: 'probationEndDate'
}
}
]
})
Please note:
- All properties are optional and can be used in any combination
- All operations are performed on
value
property
fromPattern
is not supported by sqlite
- See cast, decrypt, encoding, compare for respective details
-
And (Keyword and
) / Or (Keyword or
) Wrappers
Both and
wrapper and or
wrapper are similar in interface as both accepts array of comparator objects, only difference is and
wrapper joins these comparator objects with and clause and or
wrapper joins these comparator objects using or clause. They override junction
property for their immediate children comparator objects and can be nested inside each other to create complex conditions. Since there is no interface, below is a sample for and / or wrapper:
{ and: [ {...}, {...}, ...] }
{ or: [ {...}, {...}, ...] }
const response = await User.find({
where: {
or: [
{ salary: { between: { gt: 5000, lt: 15000 } } },
{ role: 'intern' }
],
userStatus: 1
}
})
Please note:
- Both wrappers works only with
where
and having
property
-
If Wrapper (Keyword if
):
Creates a if-else check and returns appropriate value. Below is the interface and default properties:
{
if: {
check: {...},
trueValue:'',
falseValue: '',
as: null
}
}
const response = await User.find({
select: [
{
if: {
check: { experience: { lt: 1 } },
trueValue:'Fresher',
falseValue: 'Experienced',
as: 'level'
}
}
]
})
-
Case Wrapper (Keyword case
):
Similar to a switch case, check
contains array of conditional objects, each object containing when
(condition to be checked) and then
(value to be returned if respective when
is true
). Also contains a default else
value when no condition is true
. Below is the interface with default values:
{
case: [
{
when: {...},
then: 'some value'
},
...
],
else: 'default value',
as: null
}
const response = await User.find({
select: [
{
case: [
{
when: { experience: { lt: 2 } },
then: 'Fresher'
},
{
when: { experience: { between: { gt: 2, lt: 4 } } },
then: 'Junior'
},
{
when: { experience: { between: { gt: 4, lt: 7 } } },
then: 'Mid-level'
},
],
else: 'Senior',
as: 'expertise'
}
]
})
-
Sum Wrapper (Keyword sum
):
Calculate total based on column name or condition. Can be chained to compare using comparator object. Part of aggregate methods, is executed on group of record(s). Below is the interface and default values:
{
sum: {
value: 'some column',
distinct: false,
ifNull: undefined,
cast: null,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{ sum: {
value: 'salary',
ifNull: 0,
cast: 'signed',
as: 'totalSalary'
}
}
],
groupBy: ['department'],
having: {
sum: {
value: 'salary',
ifNull: 0,
compare: { gt: 5000 }
}
}
})
-
Average Wrapper (Keyword avg
):
Calculate average based on column name or condition. Can be chained to compare using comparator object. Part of aggregate methods, is executed on group of record(s). Below is the interface and default values:
{
avg: {
value: 'some column',
distinct: false,
ifNull: undefined,
cast: null,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
avg: {
value: 'salary',
ifNull: 0,
cast: 'unsigned',
as: 'averageSalary',
}
}
],
groupBy: ['department'],
having: {
avg: {
value: 'salary',
compare: { gt: 15000 }
}
}
})
-
Count Wrapper (Keyword count
):
Calculate count based on column name or condition. Can be chained to compare using comparator object. Part of aggregate methods, is executed on group of record(s). Below is the interface and default values:
{
count: {
value: 'some column',
distinct: false,
ifNull: undefined,
cast: null,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
count: {
value: '*',
distinct: true,
ifNull: 0,
as: 'totalEmployees',
}
}
],
groupBy: ['department']
})
-
Min Wrapper (Keyword min
):
Calculate lowest value based on column name or condition. Can be chained to compare using comparator object. Part of aggregate methods, is executed on group of record(s). Below is the interface and default values:
{
min: {
value: 'some column',
distinct: false,
ifNull: undefined,
cast: null,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
min: {
value: 'salary',
ifNull: 0,
cast: 'unsigned',
as: 'lowestSalary'
}
}
]
})
-
Max Wrapper (Keyword max
):
Calculate highest value based on column name or condition. Can be chained to compare using comparator object. Part of aggregate methods, is executed on group of record(s). Below is the interface and default values:
{
max: {
value: 'some column',
distinct: false,
ifNull: undefined,
cast: null,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
max: {
value: 'salary',
distinct: true,
ifNull: 0,
cast: 'unsigned',
as: 'highestSalary'
}
}
]
})
-
Json Wrapper (Keyword json
):
Can be used to create json object/array during execution or by using values from a sub-query or combination of both, extract values from json object/array, check if json contains certain value or not. Supports full sub-query properties (similar to find
method). Part of aggregate methods, is executed on group of record(s). Below is the interface and default values:
{
json: {
value: {...},
table: null,
alias: null,
join: [],
where: {}
groupBy: [],
having: {},
orderBy: {},
limit: undefined,
offset: undefined,
extract: null,
contains: null,
aggregate: false,
decrypt: null,
cast: null,
as: null,
compare: {}
}
}
const response = await User.find({
alias: 'u',
select: [
{
json: {
value: {
orderId: 'orderId',
purchaseDate: 'createdOn',
total: 'amount',
status: 'status'
},
table: 'order_history',
where: {
userId: 'u.userId'
},
aggregate: true,
as: 'orders',
}
}
]
})
Please note:
- If
value
is object, it will create json object
- If
value
is array, it will crate json array
- If
value
can also accept column name as string
aggregate
can be set to true
to combine multiple json objects/arrays
-
Refer Wrapper (Keyword refer
)
Performs sub-query to extract value from another table, it is similar to have reference of entire find
method as a special object, with all the properties (with additional table
property) same as find
method. Below is the interface and default values:
{
table: null,
alias: null,
select: [],
join: [],
where: {}
groupBy: [],
having: {},
orderBy: {},
limit: undefined,
offset: undefined,
decrypt: null,
cast: null,
as: null,
}
const response = await User.find({
alias: 'u',
select: [
...,
{
refer: {
select: ['departmentName'],
from: 'departments_table',
where: {
departmentId: 'u.departmentId'
}
}
}
],
where: {
userStatus: 1
}
})
-
Concat Wrapper (Keyword concat
)
Used to combine (concat) multiple values using string pattern
, it is similar to str
but with multiple values.
{
concat: {
value: [],
pattern: '',
textCase: null,
padding: {
left: {
length: null,
pattern: null
},
right: {
length: null,
pattern: null
}
},
substr: {
start: 0,
length: null
},
trim: false,
as: null,
compare: {}
}
}
const response = await User.find({
select: [
{
concat: {
value: ['firstName', 'lastName'],
as: 'fullName'
}
}
]
})
4.4 Comparator Objects
UnSQL provides various objects to compare different values, as mentioned below:
eq | = | compares, key is equal to value |
notEq | != | compares, key is not equal to value |
gt | > | compares, key is greater than to value |
lt | < | compares, key is lower than to value |
gtEq | >= | compares, key is greater than to value |
ltEq | <= | compares, key is lower than to value |
between | BETWEEN ? AND ? | checks, key is in a range of values |
in | IN | checks, key has an exact match in a set of values in value |
notIn | NOT IN | checks, key does not have exact match in a set of values in value |
like | LIKE '%?%' | fuzzy search, value contains key at any position |
notLike | NOT LIKE '%?%' | fuzzy search, value does not contain key at any position |
startLike | LIKE '?%' | fuzzy search, value begins with key |
notStartLike | NOT LIKE '?%' | fuzzy search, value does not begins with key |
endLike | LIKE '%?' | fuzzy search, value ends with key |
notEndLike | NOT LIKE '%?' | fuzzy search, value does not ends with key |
5. Session Manager
Session Manager is a special class, used to create an instance of session
object. It also provides various static asynchronous methods to manage the lifecycle of a persistent (reusable) instance of transaction across multiple query execution as mentioned below:
init | initializes session (transaction ) |
rollback | undo all (un-committed) changes, reverting to the initial state |
commit | finalizes all changes, making them permanent (cannot be undone) |
close | ends the transaction and closes the session |
Please note:
- Constructor requires
connection
or connection pool
as parameter
rollback
and commit
accept an optional boolean parameter, to close session
(when true
) at this point
- When trying to combine Session Manager with
rawQuery
, it will not work with methodType: 'exec'
is set in dialect: 'sqlite'
or when executing multiple SQL statements in single query
6. Examples
6.1 Find all Users
router.get('/users', async (req, res) => {
const response = await User.find()
})
6.2 Find single User by Id
router.get('/users/:userId(\\d+)', async (req, res) => {
const { userId } = req.params
const response = await User.find({ where: { userId } })
})
6.3 Login User by email or mobile
router.post('/users/login', async (req, res) => {
const { loginId, password } = req.body
const response = await User.find({
select: [...],
where: {
or: [
{ email: `#${loginId}` },
{ mobile: `#${loginId}` }
]
}
})
})
Please note: UnSQL uses #
as prefixed to recognize string as plain text instead of column name
router.get('/users', async (req, res) => {
const response = await User.find({
select: ['userId', 'firstName',
{
json: {
value: ['#Jabalpur', '#Delhi', '#Pune'],
extract: 0
as: 'city'
}
}
]
})
})
router.get('/users', async (req, res) => {
const response = await User.find({
select: ['userId', 'firstName',
{
json: {
value: 'address',
extract: 'permanent.city'
as: 'city'
}
}
]
})
})
6.6 Fetch Users with their last 10 Orders (Join sub-query)
router.get('/users', async (req, res) => {
const response = await User.find({
alias: 'u',
select: ['userId', 'firstName',
{
json: {
value: {
orderId: 'orderId',
purchaseDate: 'createdOn',
total: 'amount',
discount: 'discount'
},
table: 'order_history',
where: {
userId: 'u.userId'
},
limit: 10,
aggregate: true
as: 'orders'
}
}
]
})
})
6.7 Save User
router.post('/users', async (req, res) => {
const data = req.body
const response = await User.save({ data })
})
6.8 Update User
router.put('/users/:userId(\\d+)', async (req, res) => {
const { userId } = req.params
const data = req.body
const response = await User.save({ data, where: { userId } })
})
6.9 Upsert User
router.post('/users', async (req, res) => {
const data = req.body
const { userId, ...upsert } = data
const response = await User.save({ data, upsert })
})
6.10 Delete User
router.delete('/users/:userId(\\d+)', async (req, res) => {
const { userId } = req.params
const response = await User.delete({ where: { userId } })
})
6.11 Delete multiple users
router.delete('/users', async (req, res) => {
const response = await User.delete({
where: {
departments: ['#sales', '#marketing']
}
})
})
Please note: UnSQL uses #
as prefixed to recognize string as plain text instead of column name
6.12 Delete all Users
router.delete('/users', async (req, res) => {
const response = await User.delete()
})
Please note: saveMode: false
is required in model config
to delete all users
6.13 Reset User table
router.delete('/users', async (req, res) => {
const response = await User.reset()
})
Please note: saveMode: false
and devMode: true
is required in model config
to use reset
6.14 Sample Session Manager
import { SessionManager } from 'unsql'
import { pool } from './path/to/your/db/service'
router.post('/orders', async (req,res) => {
const { userId } = req.params
const data = req.body
const session = new SessionManager(pool)
const initResp = await session.init()
if (!initResp.success) return res.status(400).json(initResp)
const bucketResp = await Bucket.find({ where: { userId }, session })
const orderResp = await Order.save({ data, session })
const items = bucketResp.result.map(item => item.orderId = orderResp.insertId )
const itemsResp = await OrderItems.save({ data: items, session })
const clearBucket = await Bucket.delete({ where: { userId }, session })
if(!bucketResp.success || !orderResp.success || !itemsResp.success) {
await session.rollback()
return res.status(400).json({ success: false, message: 'Error while placing order!', error: bucketResp?.error || orderResp?.error || itemsResp?.error })
}
await session.commit()
return res.status(201).json({ success: true, message: 'Order placed successfully!', orderId: orderResp.insertId })
})
7. FAQs
7.1 Difference between plain text and column name?
UnSQL uses #
as prefix to identify if string is plain text, or column name if string does not start with #
. The only exception is target
and replaceWith
properties inside replace
due to the limited of implementation for these properties by SQL they only support plain text and not columns hence prefixing them with #
is not required
7.2 Priority of secret / iv / sha defined inside config / encryption / decrypt / encrypt?
When configurations like secret
| iv
| sha
are declared in all places, encryption
at method level will override encryption
at config
, similarly decrypt
/ encrypt
inside special object will override all other.
7.3 Does UnSQL support unified codebase for all SQL dialects?
Yes, UnSQL is the only library that supports unified codebase across multiple SQL dialects so you don't have to update your code while switching between SQL dialect to another.
7.4 Are the identifiers like column and table names case sensitive?
Yes, in case of postgresql
and sqlite
, identifiers like column names and table names are case sensitive by default. In case of mysql
identifiers like table name and column name are case in-sensitive.
Support

Author