A TypeScript database abstraction library providing a unified interface for MySQL, MariaDB, PostgreSQL, and SQLite. It wraps driver-specific behavior behind a common abstract Database class so consumers use the same API regardless of backend.
Node >= 22 required.
https://gibme-npm.github.io/sql/
yarn add @gibme/sql
Use createConnection() to instantiate the correct driver based on Database.Type:
import { createConnection, Database } from '@gibme/sql';
const client = createConnection(Database.Type.SQLITE, {
filename: ':memory:'
});
await client.createTable('test', [
{ name: 'id', type: 'integer' },
{ name: 'value', type: 'varchar(255)' }
], ['id']);
const [rows, meta] = await client.query('SELECT * FROM test');
When no arguments are provided, createConnection() reads from environment variables (see Environment Variables below) and defaults to an in-memory SQLite database.
Import specific drivers directly to avoid bundling unused drivers:
import MySQL from '@gibme/sql/mysql';
import MariaDB from '@gibme/sql/mariadb';
import Postgres from '@gibme/sql/postgres';
import SQLite from '@gibme/sql/sqlite';
import { Database } from '@gibme/sql/database';
import MySQL from '@gibme/sql/mysql';
const client = new MySQL({
host: 'localhost', // default: '127.0.0.1'
port: 3306, // default: 3306
user: 'someuser', // default: ''
password: 'somepassword',
database: 'somedatabase',
connectTimeout: 30_000, // default: 30000 ms
useSSL: false, // default: false
rejectUnauthorized: false // default: false
});
The MySQL driver accepts all mariadb.PoolConfig options in addition to the ones listed above.
The second constructor argument accepts a table options string used when creating tables (default: 'ENGINE=InnoDB PACK_KEYS=1 ROW_FORMAT=COMPRESSED').
MariaDB extends the MySQL driver and accepts the same configuration. The only difference is the UPSERT dialect used internally.
import MariaDB from '@gibme/sql/mariadb';
const client = new MariaDB({
host: 'localhost',
port: 3306,
user: 'someuser',
password: 'somepassword',
database: 'somedatabase'
});
import Postgres from '@gibme/sql/postgres';
const client = new Postgres({
host: 'localhost', // default: '127.0.0.1'
port: 5432, // default: 5432
user: 'someuser', // default: ''
password: 'somepassword',
database: 'somedatabase',
ssl: false, // default: false
rejectUnauthorized: false // default: false
});
The Postgres driver accepts all pg.PoolConfig options in addition to the ones listed above.
import SQLite from '@gibme/sql/sqlite';
const client = new SQLite({
filename: './data.db', // default: ':memory:'
readonly: false, // default: false
foreignKeys: true, // default: true (enables PRAGMA foreign_keys)
WALmode: true // default: true (enables PRAGMA journal_mode=WAL)
});
SQLite instances are managed as singletons per filename, so multiple SQLite instances pointing to the same file share the underlying connection. All operations are serialized through a mutex for thread safety.
Read and set PRAGMA values on SQLite connections:
const walMode = await client.getPragma('journal_mode');
await client.setPragma('foreign_keys', true);
The following PRAGMAs are supported through the dedicated methods: quick_check, integrity_check, incremental_vacuum, foreign_key_check, foreign_key_list, index_info, index_list, index_xinfo, table_info, table_xinfo, and optimize. Other PRAGMAs can be executed directly via query().
All queries return a [rows, metadata, query] tuple:
const [rows, meta, query] = await client.query<{
column1: string,
column2: number
}>('SELECT * FROM test WHERE column1 = ?', 'value');
// rows - RecordType[] array of result rows
// meta - { changedRows, affectedRows, insertId?, length }
// query - { query, values? } the executed query
Query parameters use ? placeholders across all drivers. The Postgres driver automatically converts these to $1, $2, ... numbered parameters internally.
await client.createTable('users', [
{ name: 'id', type: 'integer', nullable: false },
{ name: 'name', type: 'varchar(255)' },
{ name: 'email', type: 'varchar(255)', unique: true },
{ name: 'role', type: 'varchar(50)', default: 'user' },
{ name: 'score', type: 'float', nullable: true }
], ['id']); // primary key columns
Tables are created with IF NOT EXISTS. Columns marked unique: true automatically get a unique index.
| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
required | Column name |
type |
string |
required | SQL type (e.g., varchar(255), integer, float) |
nullable |
boolean |
true |
Whether the column allows NULL values |
default |
string | number | boolean |
— | Default value for the column |
unique |
boolean |
false |
Creates a unique index on this column |
foreignKey |
ForeignKey |
— | Foreign key relationship (see below) |
Column types are validated against the pattern /^[a-zA-Z][a-zA-Z0-9 (),]*$/.
await client.createTable('orders', [
{ name: 'id', type: 'integer' },
{
name: 'user_id',
type: 'integer',
foreignKey: {
table: 'users',
column: 'id',
onDelete: Database.Table.ForeignKeyConstraint.CASCADE,
onUpdate: Database.Table.ForeignKeyConstraint.CASCADE
}
}
], ['id']);
Available constraints: RESTRICT, CASCADE, NULL (SET NULL), DEFAULT (SET DEFAULT), NA (NO ACTION).
// Standard index
await client.createIndex('users', ['email']);
// Unique index
await client.createIndex('users', ['email'], Database.Table.IndexType.UNIQUE);
// List all tables
const tables = await client.listTables();
// Drop tables
await client.dropTable('users');
await client.dropTable(['temp1', 'temp2']);
// Truncate tables
await client.truncate('users');
// Switch database (MySQL/MariaDB/Postgres)
await client.use('other_database');
All drivers support transactions via transaction():
const results = await client.transaction([
{ query: 'INSERT INTO users (name) VALUES (?)', values: ['Alice'] },
{ query: 'INSERT INTO users (name) VALUES (?)', values: ['Bob'] }
]);
// results is an array of [rows, metadata, query] tuples, one per query
Set noError: true on individual queries to ignore their failures without aborting the transaction (MySQL/MariaDB/Postgres only — SQLite transactions are atomic and roll back entirely on any failure).
await client.transaction([
{ query: 'INSERT INTO users (name) VALUES (?)', values: ['Alice'] },
{ query: 'INSERT INTO users (name) VALUES (?)', values: ['duplicate'], noError: true },
{ query: 'INSERT INTO users (name) VALUES (?)', values: ['Charlie'] }
]);
await client.multiInsert('test', ['col1', 'col2'], [
['a', 1],
['b', 2],
['c', 3]
]);
Insert rows or update them if they conflict on the primary key:
await client.multiUpdate('test', ['col1'], ['col1', 'col2'], [
['a', 10], // updates existing row
['d', 40] // inserts new row
]);
Both operations accept an optional useTransaction parameter (default: true) to control whether the bulk operation is wrapped in a transaction.
Generate query objects without executing them, useful for combining with other operations or inspecting the generated SQL:
const queries = client.prepareMultiInsert('test', ['col1', 'col2'], [
['a', 1],
['b', 2]
]);
const createQueries = client.prepareCreateTable('test', [
{ name: 'id', type: 'integer' }
], ['id']);
MySQL/MariaDB use the mariadb driver's native connection pool. PostgreSQL uses pg's pool. SQLite uses a singleton instance with mutex-based concurrency.
Monitor pool status via getters:
console.log(client.idleConnections); // connections available in pool
console.log(client.totalConnections); // total pool size
The Database class extends EventEmitter. Available events vary by driver:
MySQL / MariaDB:
| Event | Description |
|---|---|
error |
Connection error |
acquire |
Connection acquired from pool |
connection |
New connection created |
enqueue |
Connection request queued (pool exhausted) |
release |
Connection released back to pool |
Postgres:
| Event | Description |
|---|---|
connect |
New connection created |
acquire |
Connection acquired from pool |
remove |
Connection removed from pool |
error |
Connection error |
client.on('error', (err) => {
console.error('Database connection error:', err);
});
// Escape a string value for safe SQL interpolation
const safe = client.escape(userInput);
// Escape an identifier (table/column name)
const id = client.escapeId('column name');
// Check the driver type
if (client.type === Database.Type.POSTGRES) { /* ... */ }
console.log(client.typeName); // 'MySQL', 'MariaDB', 'Postgres', or 'SQLite'
Escaping is driver-aware: Postgres uses pg-format, all others use sqlstring.
MySQL/MariaDB and Postgres disable TLS certificate validation by default (rejectUnauthorized: false) for development convenience. Set rejectUnauthorized: true in production:
// MySQL/MariaDB
const client = new MySQL({
host: 'prod-host',
useSSL: true,
rejectUnauthorized: true
});
// Postgres
const client = new Postgres({
host: 'prod-host',
ssl: true,
rejectUnauthorized: true
});
createConnection() reads these environment variables when options are not provided directly:
| Variable | Description | Default |
|---|---|---|
SQL_TYPE |
Database type enum value (0=MySQL, 1=Postgres, 2=SQLite, 4=MariaDB) |
2 (SQLite) |
SQL_HOST |
Database host | 127.0.0.1 |
SQL_PORT |
Database port | Driver default |
SQL_USERNAME |
Database user | '' |
SQL_PASSWORD |
Database password | |
SQL_DATABASE |
Database name | |
SQL_SSL |
Enable SSL (true/false) |
false |
SQL_FILENAME |
SQLite database file path | :memory: |
A .env file is loaded automatically via dotenv.
// Database type enum
enum Database.Type {
MYSQL = 0,
POSTGRES = 1,
SQLITE = 2,
MARIADB = 4
}
// Query result tuple
type Database.Query.Result<T> = [T[], Database.Query.MetaData, Database.Query]
// Query metadata
type Database.Query.MetaData = {
changedRows: number;
affectedRows: number;
insertId?: number;
length: number;
}
// Query definition (for transactions and prepared queries)
type Database.Query = {
query: string;
values?: any[];
noError?: boolean;
}
// Column definition
type Database.Table.Column = {
name: string;
type: string;
nullable?: boolean;
default?: string | number | boolean;
unique?: boolean;
foreignKey?: Database.Table.ForeignKey;
}
// Foreign key definition
type Database.Table.ForeignKey = {
table: string;
column: string;
onUpdate?: Database.Table.ForeignKeyConstraint;
onDelete?: Database.Table.ForeignKeyConstraint;
}
// Foreign key constraint actions
enum Database.Table.ForeignKeyConstraint {
RESTRICT = 'RESTRICT',
CASCADE = 'CASCADE',
NULL = 'SET NULL',
DEFAULT = 'SET DEFAULT',
NA = 'NO ACTION'
}
// Index types
enum Database.Table.IndexType {
NONE = '',
UNIQUE = 'UNIQUE'
}
Postgres ? placeholders: The library automatically converts ? placeholders to $1, $2, ... for Postgres. This conflicts with PostgreSQL JSON operators (?, ?|, ?&). Use the native $1, $2 syntax directly for queries involving JSON operators.
SQLite transaction atomicity: SQLite transactions use better-sqlite3's native transaction() callback, which is all-or-nothing. Individual query noError flags cannot be honored per-query — if any query fails, the entire transaction rolls back.
Column type validation: Column types in createTable are validated against the pattern /^[a-zA-Z][a-zA-Z0-9 (),]*$/. Types must start with a letter and can only contain letters, digits, spaces, parentheses, and commas.