Skip to content

Repository files navigation

@stoolap/node

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.

LicenseNodeBunDeno

Installation

npm install @stoolap/node

The 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++"

Quick Start

// 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();

API

Database

// 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');

Async Methods

MethodReturnsDescription
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

Sync methods run on the main thread. Faster for simple operations but block the event loop.

MethodReturnsDescription
Database.openSync(path)DatabaseOpen a database
clone()DatabaseClone handle (shared engine, own state)
executeSync(sql, params?)RunResultExecute DML statement
execSync(sql)voidExecute a DDL statement
querySync(sql, params?)Object[]Query rows as objects
queryOneSync(sql, params?)Object | nullQuery single row
queryRawSync(sql, params?){columns, rows}Query in columnar format
executeBatchSync(sql, paramsArray)RunResultExecute with multiple param sets
beginSync()TransactionBegin a transaction
prepare(sql)PreparedStatementCreate a prepared statement
closeSync()voidClose the database

RunResult is { changes: number }. It can be imported as a type:

import{Database,RunResult}from'@stoolap/node';

Persistence

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();
Configuration

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');
Sync Modes

Controls the durability vs. performance trade-off:

ModeValueDescription
nonesync_mode=noneNo fsync. Data durable only after checkpoint
normalsync_mode=normalFsync every 1 second (batched). DDL fsyncs immediately (default)
fullsync_mode=fullFsync on every write. Maximum durability
All Configuration Parameters
ParameterDefaultDescription
sync_modenormalSync mode: none, normal, or full
checkpoint_interval60Seconds between checkpoint cycles (seal + compact + WAL truncate)
compact_threshold4Sub-target volumes per table before merging
target_volume_rows1048576Target rows per cold volume. Controls compaction split boundary
checkpoint_on_closeonSeal all hot rows on clean shutdown for fast startup
wal_compressiononLZ4 compression for WAL entries
volume_compressiononLZ4 compression for cold volume files
compressiononShorthand: set both wal_compression and volume_compression
keep_snapshots5Number of backup snapshot files to retain

Cloning

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();

Raw Query Format

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']]

Batch Execution

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);// 3

PreparedStatement

Prepared 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' }

Methods

All methods mirror Database but without the sql parameter (it's bound at prepare time).

AsyncSyncDescription
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.

Async 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 DML

Sync Prepared Statement

conststmt=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 DML

Batch with Prepared Statement

constinsert=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

Transaction

Methods

AsyncSyncDescription
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

Async Transaction

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;}

Sync Transaction

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;}

Batch in Transaction

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);// 2

Parameters

Both 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'});

Error Handling

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);}

Supported Types

JavaScriptStoolapNotes
number (integer)INTEGER
number (float)FLOAT
stringTEXT
booleanBOOLEAN
null / undefinedNULL
BigIntINTEGER
DateTIMESTAMP
Float32ArrayVECTOR(N)Returned as Float32Array
BufferTEXT (UTF-8)
Object / ArrayJSON (stringified)

Vector Support

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.

Building from Source

Requires:

  • Node.js >= 18
  • C compiler (gcc, clang, or MSVC)
  • node-gyp and its prerequisites

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 test

License

Apache 2.0 - see LICENSE for details.

About

Stoolap NodeJS Bindings

Topics

Resources

Stars

20 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages