@cqlite/node
v0.15.0
Published
Node.js bindings for CQLite - read Apache Cassandra 5.0 SSTables without cluster dependencies
Maintainers
Readme
@cqlite/node
Node.js bindings for CQLite - a high-performance library for reading Apache Cassandra 5.0 SSTable files locally, without requiring a running Cassandra cluster.
Installation
npm install @cqlite/nodeQuick Start
import { Database } from '@cqlite/node';
// Open a database with schema
const db = await Database.open('path/to/sstables', { schema: 'schema.cql' });
// Execute queries (executeNative() returns native JS types with full precision)
const result = await db.executeNative('SELECT * FROM keyspace.table LIMIT 10');
for (const row of result.rows) {
console.log(row.name);
}
await db.close();⚠️ Warning:
execute()is deprecated and will be removed in the next major. PreferexecuteNative().execute()returns lossy legacy JSON encodings:
blob→ base64 string (not aBuffer)timestamp→ ISO-8601 string (not aDate)varint→"0x{hex}"stringdecimal→"decimal:{scale}:0x{hex}"stringdate/time→ number (days-since-epoch / nanoseconds-since-midnight)It is also slower (JSON off-loop, then JS on-loop — a double conversion). Calling
execute()emits a one-timeDeprecationWarning. UseexecuteNative()for native types (BigInt,Buffer,Date,Set,Map) with full fidelity. (bigint/countercurrently come back as an exactBigInton this napi build, so they are not presently rounded — butexecute()is unsupported regardless.)
Features
- Zero cluster dependency - Read SSTable files directly from disk
- Full CQL type support - All primitive types, collections, UDTs, and frozen types
- Native JavaScript types - BigInt, Date, Buffer, Set, Map via
executeNative() - Memory-efficient streaming - Configure buffer sizes for large datasets
- Thread-safe - Safe concurrent access from multiple workers
- Cross-platform - Linux (x86_64, ARM64), macOS (Intel, Apple Silicon), Windows
Supported Platforms
| Platform | Architecture | Status | |----------|--------------|--------| | Linux | x86_64 | ✅ | | Linux | ARM64 | ✅ | | macOS | Intel (x86_64) | ✅ | | macOS | Apple Silicon | ✅ | | Windows | x64 | ✅ |
Requirements
- Node.js 18+
- Cassandra 5.0 SSTable files
API Reference
Opening a Database
import { Database } from '@cqlite/node';
// With schema file
const db = await Database.open('/path/to/sstables', {
schema: '/path/to/schema.cql',
});
// Always close when done
await db.close();The close() method is idempotent - safe to call multiple times.
Executing Queries
// Simple query - executeNative() returns native JS types with full precision
const result = await db.executeNative('SELECT * FROM keyspace.table');
for (const row of result.rows) {
console.log(row);
}
// With LIMIT
const limited = await db.executeNative('SELECT name, age FROM users LIMIT 100');
// Access query metadata
console.log(`Rows returned: ${result.rowCount}`);
console.log(`Execution time: ${result.executionTimeMs}ms`);
console.log(`Columns: ${result.columns.map(c => c.name).join(', ')}`);Prefer
executeNative()over the deprecatedexecute()— see the warning at the top of this README for the precision/encoding hazards ofexecute().
Native Types with executeNative()
Use executeNative() to get native JavaScript types instead of JSON-serializable values:
const result = await db.executeNative('SELECT * FROM keyspace.table');
for (const row of result.rows) {
// BigInt for CQL bigint/varint
const balance: bigint = row.balance;
// Date for CQL timestamp
const created: Date = row.created;
// Buffer for CQL blob
const data: Buffer = row.blob_data;
// Set for CQL set
const tags: Set<string> = row.tags;
// Map for CQL map
const metadata: Map<string, string> = row.metadata;
}JSON Encoding (deprecated execute() method)
⚠️ Deprecated — removed in the next major.
execute()returns lossy legacy JSON encodings and is slower thanexecuteNative(). In particular blob comes back as a base64 string, timestamp as an ISO-8601 string, and varint/decimal as bespoke non-round-trippable strings. This section documents the encoding for the few callers that still depend on it; new code should useexecuteNative().
The execute() method returns JSON-serializable values. For most types this works intuitively,
but varint and decimal types use a hex-based encoding to preserve arbitrary precision:
// Using execute() - hex encoding (deprecated)
const result = await db.execute('SELECT amount FROM transactions');
console.log(result.rows[0].amount);
// Varint: "0x7f" (127), "0xff" (-1), "0x0100" (256)
// Decimal: "decimal:2:0x7b" (1.23), "decimal:2:0xee29" (-45.67)
// Using executeNative() - proper types (recommended)
const native = await db.executeNative('SELECT amount FROM transactions');
console.log(native.rows[0].amount);
// Varint: 127n (BigInt)
// Decimal: "1.23" (human-readable string)Hex Format Details:
varint:"0x{hex}"- Two's complement big-endian hex encodingdecimal:"decimal:{scale}:0x{hex}"- Scale (decimal places) + hex-encoded unscaled value
Recommendation: Use executeNative(). The execute() method is deprecated
(removed in the next major); its JSON encoding is lossy (blob/timestamp/varint/
decimal come back as bespoke strings) and it is slower than executeNative().
Column Metadata
Each query result includes column information:
const result = await db.executeNative('SELECT * FROM keyspace.table');
for (const col of result.columns) {
console.log(`${col.name}: ${col.dataType}`);
console.log(` nullable: ${col.nullable}`);
console.log(` position: ${col.position}`);
}Database Statistics
const stats = await db.getStats();
console.log(`SSTables: ${stats.totalSstables}`);
console.log(`Total rows: ${stats.totalRows}`);
console.log(`Memory: ${stats.memoryUsedBytes} bytes`);Refreshing SSTables (v0.13)
If Cassandra (or another process) writes new SSTables while your Database handle
is open, call refresh() to re-discover them. Refresh is explicit-only (CQLite
never rescans behind your back) and atomic / fail-closed: if any newly found
generation fails to open, the swap is rolled back and the handle keeps serving the
prior, consistent set of readers.
// ... time passes; Cassandra flushes/compacts new SSTables to disk ...
const report = await db.refresh();
console.log(`Tables scanned: ${report.tablesScanned}`);
console.log(`Readers added: ${report.readersAdded}`);
console.log(`Readers removed: ${report.readersRemoved}`);
// Subsequent queries see the newly discovered data
const result = await db.executeNative('SELECT * FROM keyspace.table');refresh(): Promise<RefreshReport> resolves to a RefreshReport with the numeric
fields tablesScanned, readersAdded, and readersRemoved.
Result Byte Budget (v0.13)
Non-streaming queries are bounded by a result-size budget of 64 MiB by default.
When the materialized result's running byte estimate exceeds the budget, the query
rejects with a CqliteError whose code === 'QUERY', directing you to add a
LIMIT clause or use executeStreaming(). Streaming queries are not subject
to this budget.
try {
const result = await db.executeNative('SELECT * FROM keyspace.big_table');
for (const row of result.rows) {
process(row);
}
} catch (e) {
if (e.code === 'QUERY') {
// Result exceeded the 64 MiB byte budget — add a LIMIT or stream instead
for await (const row of db.executeStreaming('SELECT * FROM keyspace.big_table')) {
process(row);
}
} else {
throw e;
}
}OpenTelemetry Tracing (v0.13)
CQLite can emit OpenTelemetry traces when built with the observability Cargo
feature; without that feature the configuration is accepted but is a no-op. Pass an
otel option to Database.open():
const db = await Database.open('path/to/sstables', {
schema: 'schema.cql',
otel: {
enabled: true, // default false
endpoint: 'http://localhost:4317', // default 'http://localhost:4317'
protocol: 'grpc', // 'grpc' (default) or 'http'
serviceName: 'cqlite', // default 'cqlite'
serviceVersion: '0.13.0', // default: package version
samplingRatio: 1.0, // default 1.0
timeoutMs: 10000, // default 10000
},
});Options are layered over the CQLITE_OTEL_* environment variables.
Error Handling
All errors include structured metadata for programmatic handling:
import { Database } from '@cqlite/node';
try {
const db = await Database.open('/path/to/data');
const result = await db.executeNative('SELECT * FROM keyspace.table');
} catch (e) {
// Error code for programmatic handling
console.log(`Code: ${e.code}`); // 'IO', 'SCHEMA', 'QUERY', 'PARSE', etc.
// Error category
console.log(`Category: ${e.category}`); // 'System', 'Schema', 'Query', etc.
// Whether the operation can be retried
console.log(`Recoverable: ${e.isRecoverable}`);
// Original error message
console.log(`Message: ${e.message}`);
}Error Codes:
| Code | Category | Description | Recoverable |
|------|----------|-------------|-------------|
| IO | System | File system errors | Yes |
| SCHEMA | Schema | Schema parsing/validation | No |
| QUERY | Query | Query execution failures | No |
| PARSE | Data | CQL syntax errors | No |
| CONFIG | Configuration | Invalid configuration | No |
| STORAGE | Storage | Storage engine errors | No |
| NOT_FOUND | NotFound | Table/resource not found | No |
| INVALID_INPUT | Logic | Invalid operation (e.g., closed db) | No |
Type Conversions
CQL types are automatically converted to JavaScript types:
| CQL Type | JavaScript Type | Notes |
|----------|-----------------|-------|
| text, varchar, ascii | string | |
| int, smallint, tinyint | number | |
| bigint, varint, counter | bigint | via executeNative() |
| float, double | number | |
| decimal | string | Preserves precision |
| boolean | boolean | |
| blob | Buffer | via executeNative() |
| timestamp | Date | via executeNative() |
| date | Date | via executeNative() |
| time | bigint | Nanoseconds since midnight, via executeNative() |
| duration | object | { months, days, nanos } |
| uuid, timeuuid | string | Lowercase formatted |
| inet | string | IP address string |
| list<T> | T[] | |
| set<T> | Set<T> | via executeNative() |
| map<K,V> | Map<K,V> | via executeNative() |
| tuple<...> | [...] | Array |
| frozen<T> | Inner type | Unwrapped |
| UDT | object | With _type and _keyspace fields |
* Note: With execute(), varint returns "0x{hex}" and decimal returns "decimal:{scale}:0x{hex}".
Use executeNative() for human-readable formats.
Write Operations
CQLite v0.9.0 adds write support to the Node.js bindings. Open the database with
writable: true and a writeDir to enable write operations.
const { Database } = require('@cqlite/node');
const db = await Database.open('path/to/sstables', {
schema: 'schema.cql',
writable: true,
writeDir: '/tmp/my-writes',
});
// Write rows via CQL INSERT, UPDATE, or DELETE
await db.executeNative(
"INSERT INTO test_basic.simple_table (id, name, age) " +
"VALUES (22222222-2222-2222-2222-222222222222, 'Bob', 25)"
);
await db.executeNative(
"UPDATE test_basic.simple_table SET age = 26 " +
"WHERE id = 22222222-2222-2222-2222-222222222222"
);
// Flush the in-memory write buffer (memtable) to an SSTable on disk.
// Returns the path to the flushed Data.db file, or "" if memtable was empty.
const path = await db.flushRun();
console.log('Flushed to:', path);
// Run background compaction within a time budget
const report = await db.maintenanceStep({ budgetMs: 100 });
console.log(`Merged ${report.rowsMerged} rows in ${report.timeSpentMs}ms`);
if (report.pendingCompaction) {
console.log('More compaction work available');
}
// Inspect write statistics (synchronous getter)
const stats = db.writeStats;
console.log('Memtable size:', stats.memtableSizeBytes, 'bytes');
console.log('Total flushed:', stats.totalWrittenBytes, 'bytes');
await db.close();Write API
| Method / Property | Description |
|-------------------|-------------|
| db.executeNative(cql) | Execute a CQL INSERT, UPDATE, or DELETE statement (recommended) |
| db.execute(cql) | Deprecated (removed next major; emits a DeprecationWarning). Same DML behavior as executeNative(), but lossy for SELECT — use executeNative() |
| db.flushRun() | Flush memtable to SSTable; returns the Data.db path or "" if memtable was empty |
| db.maintenanceStep(options?) | Run STCS compaction for up to options.budgetMs ms (default: 100); returns MaintenanceReport |
| db.writeStats | Synchronous getter: memtableSizeBytes, memtableRowCount, totalWrittenBytes, l0SstableCount |
Known Limitations
- Counter columns cannot be written —
execute()throwsCqliteErrorfor counter mutations. - BTI-format index files are not produced; the writer emits BIG format.
See docs/write-support-limitations.md for the full limitations reference.
Examples
See the examples/ directory for complete working examples:
- basic-query.ts - Simple SELECT queries
- type-handling.ts - Working with CQL types
- error-handling.ts - Error handling patterns
- streaming.ts - Large result handling
- performance.ts - Memory-optimized usage
Streaming concurrency caveat:
executeStreaming()fetches each batch ofK = bufferSizerows on a libuv threadpool thread, so N concurrent streams can each occupy a libuv threadpool thread for the duration of a batch fetch. Heavy concurrentfs/cryptowork in the same process may see added latency until the follow-up (#1901) moves streaming off the libuv pool onto the tokio runtime. If an error occurs mid-stream, rows already read in the in-flight batch (up tobufferSize) are not delivered — the iterator rejects with the error (errors are terminal).
Resources
- TypeScript Definitions - Complete API type hints
- Main Project README - CQLite project overview
- Write Support Guide - Detailed write documentation
- Issue Tracker - Report bugs or request features
License
MIT OR Apache-2.0
