High-performance JavaScript driver for Stoolap, a modern embedded SQL database with MVCC, time-travel queries, and full ACID compliance.
Built with a native N-API C addon for minimal overhead. Works with Node.js, Bun, and Deno. Provides both async and sync APIs.
npm install @stoolap/nodeThe stoolap engine shared library is pre-built for:
- macOS (x64, ARM64)
- Linux (x64, ARM64 GNU)
- Windows (x64 MSVC)
A C compiler is required to build the thin N-API addon on install (compiled automatically via node-gyp):
- macOS:
xcode-select --install - Linux:
sudo apt-get install build-essential(or equivalent) - Windows: Visual Studio Build Tools with "Desktop development with C++"
// ESMimport{Database}from'@stoolap/node';// CommonJSconst{ Database }=require('@stoolap/node');constdb=awaitDatabase.open(':memory:');awaitdb.exec(` CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT )`);// Insert with positional parameters ($1, $2, ...)awaitdb.execute('INSERT INTO users (id, name, email) VALUES ($1, $2, $3)',[1,'Alice','alice@example.com']);// Insert with named parameters (:key)awaitdb.execute('INSERT INTO users (id, name, email) VALUES (:id, :name, :email)',{id: 2,name: 'Bob',email: 'bob@example.com'});// Query rows as objectsconstusers=awaitdb.query('SELECT * FROM users ORDER BY id');// [{ id: 1, name: 'Alice', email: 'alice@example.com' }, ...]// Query single rowconstuser=awaitdb.queryOne('SELECT * FROM users WHERE id = $1',[1]);// { id: 1, name: 'Alice', email: 'alice@example.com' }// Query in raw columnar format (faster, no per-row object creation)constraw=awaitdb.queryRaw('SELECT id, name FROM users ORDER BY id');// { columns: ['id', 'name'], rows: [[1, 'Alice'], [2, 'Bob']] }awaitdb.close();// In-memoryconstdb=awaitDatabase.open(':memory:');constdb=awaitDatabase.open('');constdb=awaitDatabase.open('memory://');// File-based (data persists across restarts)constdb=awaitDatabase.open('./mydata');constdb=awaitDatabase.open('file:///absolute/path/to/db');| Method | Returns | Description |
|---|---|---|
Database.open(path) | Promise<Database> | Open a database |
execute(sql, params?) | Promise<RunResult> | Execute DML statement |
exec(sql) | Promise<void> | Execute a DDL statement |
query(sql, params?) | Promise<Object[]> | Query rows as objects |
queryOne(sql, params?) | Promise<Object | null> | Query single row |
queryRaw(sql, params?) | Promise<{columns, rows}> | Query in columnar format |
begin() | Promise<Transaction> | Begin a transaction |
close() | Promise<void> | Close the database |
Sync methods run on the main thread. Faster for simple operations but block the event loop.
| Method | Returns | Description |
|---|---|---|
Database.openSync(path) | Database | Open a database |
clone() | Database | Clone handle (shared engine, own state) |
executeSync(sql, params?) | RunResult | Execute DML statement |
execSync(sql) | void | Execute a DDL statement |
querySync(sql, params?) | Object[] | Query rows as objects |
queryOneSync(sql, params?) | Object | null | Query single row |
queryRawSync(sql, params?) | {columns, rows} | Query in columnar format |
executeBatchSync(sql, paramsArray) | RunResult | Execute with multiple param sets |
beginSync() | Transaction | Begin a transaction |
prepare(sql) | PreparedStatement | Create a prepared statement |
closeSync() | void | Close the database |
RunResult is { changes: number }. It can be imported as a type:
import{Database,RunResult}from'@stoolap/node';File-based databases persist data to disk using WAL (Write-Ahead Logging) and an immutable volume-based storage engine. Hot data lives in memory, cold data is sealed into columnar .vol files with zone maps, bloom filters, and LZ4 compression. Data survives process restarts.
constdb=awaitDatabase.open('./mydata');awaitdb.exec('CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)');awaitdb.execute('INSERT INTO kv VALUES ($1, $2)',['hello','world']);awaitdb.close();// Reopen: data is still thereconstdb2=awaitDatabase.open('./mydata');constrow=awaitdb2.queryOne('SELECT * FROM kv WHERE key = $1',['hello']);// { key: 'hello', value: 'world' }awaitdb2.close();Pass configuration as query parameters in the path:
// Maximum durability: fsync on every writeconstdb=awaitDatabase.open('./mydata?sync_mode=full');// High throughput: no fsync, data durable at checkpointconstdb=awaitDatabase.open('./mydata?sync_mode=none');// Custom checkpoint interval with compressionconstdb=awaitDatabase.open('./mydata?checkpoint_interval=60&compression=on');// Multiple optionsconstdb=awaitDatabase.open('./mydata?sync_mode=normal&checkpoint_interval=120&compact_threshold=4');Controls the durability vs. performance trade-off:
| Mode | Value | Description |
|---|---|---|
none | sync_mode=none | No fsync. Data durable only after checkpoint |
normal | sync_mode=normal | Fsync every 1 second (batched). DDL fsyncs immediately (default) |
full | sync_mode=full | Fsync on every write. Maximum durability |
| Parameter | Default | Description |
|---|---|---|
sync_mode | normal | Sync mode: none, normal, or full |
checkpoint_interval | 60 | Seconds between checkpoint cycles (seal + compact + WAL truncate) |
compact_threshold | 4 | Sub-target volumes per table before merging |
target_volume_rows | 1048576 | Target rows per cold volume. Controls compaction split boundary |
checkpoint_on_close | on | Seal all hot rows on clean shutdown for fast startup |
wal_compression | on | LZ4 compression for WAL entries |
volume_compression | on | LZ4 compression for cold volume files |
compression | on | Shorthand: set both wal_compression and volume_compression |
keep_snapshots | 5 | Number of backup snapshot files to retain |
clone() creates a new Database handle that shares the same underlying engine (data, indexes, transactions) but has its own executor and error state. Useful for concurrent access patterns such as worker threads.
constdb=awaitDatabase.open('./mydata');constdb2=db.clone();// Both see the same dataawaitdb.execute('INSERT INTO users VALUES ($1, $2)',[1,'Alice']);constrow=db2.queryOneSync('SELECT * FROM users WHERE id = $1',[1]);// { id: 1, name: 'Alice' }// Each clone must be closed independentlyawaitdb2.close();awaitdb.close();queryRaw / queryRawSync return { columns: string[], rows: any[][] } instead of an array of objects. Faster when you don't need named keys.
constraw=db.queryRawSync('SELECT id, name, email FROM users ORDER BY id');console.log(raw.columns);// ['id', 'name', 'email']console.log(raw.rows);// [[1, 'Alice', 'alice@example.com'], [2, 'Bob', 'bob@example.com']]Execute the same SQL with multiple parameter sets in a single call. Automatically wraps in a transaction.
constresult=db.executeBatchSync('INSERT INTO users VALUES ($1, $2, $3)',[[1,'Alice','alice@example.com'],[2,'Bob','bob@example.com'],[3,'Charlie','charlie@example.com'],]);console.log(result.changes);// 3Prepared statements parse SQL once and reuse the cached execution plan on every call. No parsing or cache lookup overhead per execution.
constinsert=db.prepare('INSERT INTO users VALUES ($1, $2, $3)');insert.executeSync([1,'Alice','alice@example.com']);insert.executeSync([2,'Bob','bob@example.com']);constlookup=db.prepare('SELECT * FROM users WHERE id = $1');constuser=lookup.queryOneSync([1]);// { id: 1, name: 'Alice', email: 'alice@example.com' }All methods mirror Database but without the sql parameter (it's bound at prepare time).
| Async | Sync | Description |
|---|---|---|
execute(params?) | executeSync(params?) | Execute DML statement |
query(params?) | querySync(params?) | Query rows as objects |
queryOne(params?) | queryOneSync(params?) | Query single row |
queryRaw(params?) | queryRawSync(params?) | Query in columnar format |
executeBatchSync(paramsArray) | Execute with multiple param sets | |
finalize() | Release the prepared statement |
Property: sql returns the SQL text of this prepared statement.
conststmt=db.prepare('SELECT * FROM users WHERE id = $1');constrows=awaitstmt.query([1]);constone=awaitstmt.queryOne([1]);constraw=awaitstmt.queryRaw([1]);constresult=awaitstmt.execute([1]);// for DMLconststmt=db.prepare('SELECT * FROM users WHERE id = $1');constrows=stmt.querySync([1]);constone=stmt.queryOneSync([1]);constraw=stmt.queryRawSync([1]);constresult=stmt.executeSync([1]);// for DMLconstinsert=db.prepare('INSERT INTO users VALUES ($1, $2, $3)');constresult=insert.executeBatchSync([[1,'Alice','alice@example.com'],[2,'Bob','bob@example.com'],[3,'Charlie','charlie@example.com'],]);console.log(result.changes);// 3| Async | Sync | Description |
|---|---|---|
execute(sql, params?) | executeSync(sql, params?) | Execute DML statement |
query(sql, params?) | querySync(sql, params?) | Query rows as objects |
queryOne(sql, params?) | queryOneSync(sql, params?) | Query single row |
queryRaw(sql, params?) | queryRawSync(sql, params?) | Query in columnar format |
commit() | commitSync() | Commit the transaction |
rollback() | rollbackSync() | Rollback the transaction |
executeBatchSync(sql, paramsArray) | Execute with multiple param sets |
consttx=awaitdb.begin();try{awaittx.execute('INSERT INTO users VALUES ($1, $2, $3)',[1,'Alice','alice@example.com']);awaittx.execute('INSERT INTO users VALUES ($1, $2, $3)',[2,'Bob','bob@example.com']);// Read within the transaction (sees uncommitted changes)constrows=awaittx.query('SELECT * FROM users');constone=awaittx.queryOne('SELECT * FROM users WHERE id = $1',[1]);constraw=awaittx.queryRaw('SELECT id, name FROM users');awaittx.commit();}catch(e){awaittx.rollback();throwe;}consttx=db.beginSync();try{tx.executeSync('INSERT INTO users VALUES ($1, $2, $3)',[1,'Alice','alice@example.com']);tx.executeSync('INSERT INTO users VALUES ($1, $2, $3)',[2,'Bob','bob@example.com']);constrows=tx.querySync('SELECT * FROM users');constone=tx.queryOneSync('SELECT * FROM users WHERE id = $1',[1]);constraw=tx.queryRawSync('SELECT id, name FROM users');tx.commitSync();}catch(e){tx.rollbackSync();throwe;}consttx=db.beginSync();constresult=tx.executeBatchSync('INSERT INTO users VALUES ($1, $2, $3)',[[1,'Alice','alice@example.com'],[2,'Bob','bob@example.com'],]);tx.commitSync();console.log(result.changes);// 2Both positional and named parameters are supported across all methods:
// Positional ($1, $2, ...)db.querySync('SELECT * FROM users WHERE id = $1 AND name = $2',[1,'Alice']);// Named (:key)db.querySync('SELECT * FROM users WHERE id = :id AND name = :name',{id: 1,name: 'Alice'});All methods throw on errors (invalid SQL, constraint violations, etc.):
// Asynctry{awaitdb.execute('INSERT INTO users VALUES ($1, $2)',[1,null]);// NOT NULL violation}catch(err){console.error(err.message);}// Synctry{db.executeSync('SELECTX * FROM users');// syntax error}catch(err){console.error(err.message);}| JavaScript | Stoolap | Notes |
|---|---|---|
number (integer) | INTEGER | |
number (float) | FLOAT | |
string | TEXT | |
boolean | BOOLEAN | |
null / undefined | NULL | |
BigInt | INTEGER | |
Date | TIMESTAMP | |
Float32Array | VECTOR(N) | Returned as Float32Array |
Buffer | TEXT (UTF-8) | |
Object / Array | JSON (stringified) |
Stoolap supports native vector storage and similarity search. Vectors are returned as Float32Array and can be passed as Float32Array bind parameters.
// Create a table with a vector columnawaitdb.exec('CREATE TABLE embeddings (id INTEGER PRIMARY KEY, vec VECTOR(3))');// Insert vectors via SQL string literalsawaitdb.execute("INSERT INTO embeddings VALUES (1, '[0.1, 0.2, 0.3]')");// Query: vectors are returned as Float32Arrayconstrow=awaitdb.queryOne('SELECT vec FROM embeddings WHERE id = 1');console.log(row.vec);// Float32Array(3) [0.1, 0.2, 0.3]console.log(row.vecinstanceofFloat32Array);// true// k-NN search with distance functionsconstnearest=awaitdb.query(` SELECT id, VEC_DISTANCE_L2(vec, '[0.15, 0.25, 0.35]') AS dist FROM embeddings ORDER BY dist LIMIT 5`);// HNSW index for fast approximate nearest neighbor searchawaitdb.exec('CREATE INDEX idx ON embeddings(vec) USING HNSW');Available distance functions: VEC_DISTANCE_L2, VEC_DISTANCE_COSINE, VEC_DISTANCE_IP.
See the Stoolap Vector Search docs for full details on HNSW indexes, distance metrics, and configuration.
Requires:
The stoolap shared library (libstoolap.dylib / libstoolap.so / stoolap.dll) must be available, either via a platform package or built from the Stoolap repository.
git clone https://github.com/stoolap/stoolap-node.git
cd stoolap-node
npm install
npm testApache 2.0 - see LICENSE for details.