@lossless.org/nosqldb
A MongoDB-wire-compatible embedded database server powered by a native Rust MVCC engine. It supports the documented command surface through the official mongodb driver without an external MongoDB server. Memory and file storage use the same paged engine, including the operation log, point-in-time revert and debug dashboard. Matching executables are bundled with the package.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @lossless.org/nosqldb
Moving from @push.rocks/smartdb
The module has moved to @lossless.org/nosqldb. Install the new package and update imports and product-specific names together:
| Previous name | New name |
|---|---|
@push.rocks/smartdb |
@lossless.org/nosqldb |
SmartdbServer |
NoSqlDbServer |
LocalSmartDb |
LocalNoSqlDb |
SmartdbStorageRehearsal |
NoSqlDbStorageRehearsal |
SmartdbDebugServer / SmartdbDebugUi |
NoSqlDbDebugServer / NoSqlDbDebugUi |
ISmartdb*, ISmartDb*, TSmartdb*, TSmartDb* |
INoSqlDb*, TNoSqlDb* |
createSmartDb*, normalizeSmartDb*, smartDb* |
createNoSqlDb*, normalizeNoSqlDb*, noSqlDb* |
SMARTDB_* environment variables |
NOSQLDB_* environment variables |
<smartdb-debugui> / /api/smartdb/* |
<nosqldb-debugui> / /api/nosqldb/* |
Benchmark --engine smartdb |
Benchmark --engine nosqldb |
The ./debugui and ./debugserver package subpaths stay the same. The new module exports the new API names; it does not export aliases for the previous names. Update explicit binary overrides to NOSQLDB_RUST_BINARY, and rename the release-builder environment variables shown below.
This is a package and API rename, not a storage-format conversion. Existing current-format v7 databases, authentication records, allocation identities, export formats, and operation receipts retain their original byte formats. Versioned smartdb.* format identifiers, the smartdb allocation provider, hash domains, and private locking/workspace names deliberately remain unchanged. Do not edit those values in stored data or rewrite existing receipts. The native rustdb executable, rustdb_* packaged artifact names, and .__rustdb_* storage directories also keep their existing identities. Earlier incompatible storage formats remain subject to the admission restrictions documented below.
What It Does
@lossless.org/nosqldb is a real database server that speaks the wire protocol used by MongoDB drivers. The core engine is written in Rust for high performance, with a thin TypeScript orchestration layer. Connect with the standard mongodb Node.js driver — no mocks, no stubs, no external binaries required.
Why NoSQLDB?
| NoSQLDB | External DB Server | |
|---|---|---|
| Startup | Managed by the application | Managed separately |
| Binary distribution | Matching executables bundled in the package | Separate installation |
| Install | pnpm add |
System package / Docker |
| Persistence | Memory or file-based | Full disk engine |
| Debug UI | Built in | External tooling |
| Point-in-time revert | Built in | Depends on the database |
| Perfect for | Unit tests, CI/CD, prototyping, local dev, embedded | Production at scale |
Three Ways to Use It
- 🎯
LocalNoSqlDb— Zero-config convenience. Give it a folder path, get a persistent database over a Unix socket. Done. - 🏗️
NoSqlDbServer— Full control. Configure port, host, storage backend, Unix sockets. Great for test fixtures or custom setups. - 🖥️
NoSqlDbDebugServer— Launch a web dashboard to visually browse collections, inspect the operation log, and revert to any point in time.
Architecture: TypeScript + Rust 🦀
NoSQLDB uses a sidecar binary pattern — TypeScript handles lifecycle, Rust handles all database operations:
┌──────────────────────────────────────────────────────────────┐
│ Your Application │
│ (TypeScript / Node.js) │
│ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ NoSqlDbServer │─────▶│ RustDbBridge (IPC) │ │
│ │ or LocalNoSqlDb │ │ @push.rocks/smartrust │ │
│ └──────────────────┘ └───────────┬───────────────┘ │
└────────────────────────────────────────┼─────────────────────┘
│ spawn + JSON IPC
▼
┌──────────────────────────────────────────────────────────────┐
│ rustdb binary │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Wire Protocol│→ │Command Router│→ │ Handlers │ │
│ │ (OP_MSG) │ │ (40+ cmds) │ │ Find,Insert.. │ │
│ └──────────────┘ └──────────────┘ └───────┬───────┘ │
│ │ │
│ ┌─────────┐ ┌────────┐ ┌───────────┐ ┌──────┴──────┐ │
│ │ Query │ │ Update │ │Aggregation│ │ Index │ │
│ │ Matcher │ │ Engine │ │ Engine │ │ Engine │ │
│ └─────────┘ └────────┘ └───────────┘ └─────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────┐ │
│ │ Native MVCC pages│ │ Memory/file I/O │ │ OpLog │ │
│ └──────────────────┘ └──────────────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────┘
▲
│ TCP / Unix Socket (wire protocol)
│
┌─────────────┴────────────────────────────────────────────────┐
│ MongoClient (mongodb npm driver) │
│ Connects directly to Rust binary │
└──────────────────────────────────────────────────────────────┘
The TypeScript layer handles lifecycle only (start/stop/configure via IPC). All database operations flow directly from the MongoClient to the Rust binary over TCP or Unix sockets — zero per-query IPC overhead.
Quick Start
Option 1: LocalNoSqlDb (Zero Config) 🎯
The fastest way to get a persistent local database:
import { LocalNoSqlDb } from '@lossless.org/nosqldb';
import { MongoClient } from 'mongodb';
// Point it at a folder — that's it
const db = new LocalNoSqlDb({ folderPath: './my-data' });
const { connectionUri } = await db.start();
// Connect with the standard driver
const client = new MongoClient(connectionUri, { directConnection: true });
await client.connect();
// Use it like any wire-protocol-compatible database
const users = client.db('myapp').collection('users');
await users.insertOne({ name: 'Alice', email: 'alice@example.com' });
const user = await users.findOne({ name: 'Alice' });
console.log(user); // { _id: ObjectId(...), name: 'Alice', email: 'alice@example.com' }
// Data persists to disk automatically — survives restarts!
await client.close();
await db.stop();
Option 2: NoSqlDbServer (Full Control) 🏗️
import { NoSqlDbServer } from '@lossless.org/nosqldb';
import { MongoClient } from 'mongodb';
// TCP mode
const server = new NoSqlDbServer({ port: 27017 });
await server.start();
const client = new MongoClient('mongodb://127.0.0.1:27017');
await client.connect();
const db = client.db('myapp');
await db.collection('users').insertOne({ name: 'Alice', age: 30 });
const user = await db.collection('users').findOne({ name: 'Alice' });
await client.close();
await server.stop();
Option 3: Debug Server (Visual Dashboard) 🖥️
Launch a web-based dashboard to inspect your database in real time:
debugserver and debugui are optional subpath exports. Install their
debug dependencies only when you use them:
pnpm add '@api.global/typedserver@^8' '@design.estate/dees-element@^2'
import { NoSqlDbServer } from '@lossless.org/nosqldb';
import { NoSqlDbDebugServer } from '@lossless.org/nosqldb/debugserver';
const server = new NoSqlDbServer({ storage: 'memory' });
await server.start();
const debugServer = new NoSqlDbDebugServer(server, { port: 4000 });
await debugServer.start();
// Open http://localhost:4000 in your browser 🚀
The debug dashboard gives you:
- 📊 Dashboard — server status, uptime, database/collection counts, operation breakdown
- 📁 Collection Browser — browse databases, collections, and documents interactively
- 📝 OpLog Timeline — every insert, update, and delete with expandable field-level diffs
- ⏪ Point-in-Time Revert — select any oplog sequence, preview what will be undone, and execute
📝 Operation Log & Point-in-Time Revert
The native engine provides these APIs for both memory and file storage. Revert publishes one atomic indexed data change and preserves authentication, fencing, publication holds and durable retry history.
Every write operation (insert, update, delete) is automatically recorded in an in-memory operation log (OpLog) with full before/after document snapshots. The OpLog lives in RAM and resets on restart — it covers the current session only, and retention is bounded (default: 10,000 entries or 64 MiB, whichever is hit first; configurable via the oplog server option). Once a limit is exceeded, the oldest entries are evicted. This enables:
- Change tracking — see exactly what changed, when, and in which collection
- Field-level diffs — compare previous and new document states
- Point-in-time revert — undo operations back to any retained sequence number
- Dry-run preview — see what would be reverted before executing
Programmatic OpLog API
import { NoSqlDbServer } from '@lossless.org/nosqldb';
const server = new NoSqlDbServer({ port: 27017 });
await server.start();
// ... perform some CRUD operations via MongoClient ...
// Get oplog entries
const oplog = await server.getOpLog({ limit: 50 });
console.log(oplog.entries);
// [{ seq: 1, op: 'insert', db: 'myapp', collection: 'users', document: {...}, previousDocument: null }, ...]
// Get aggregate stats
const stats = await server.getOpLogStats();
console.log(stats);
// { currentSeq: 42, totalEntries: 42, oldestSeq: 1, approxBytes: 18342, entriesByOp: { insert: 20, update: 15, delete: 7 } }
// Preview a revert (dry run)
const preview = await server.revertToSeq(30, true);
console.log(`Would undo ${preview.reverted} operations`);
// Execute the revert — undoes all operations after seq 30
const result = await server.revertToSeq(30, false);
console.log(`Reverted ${result.reverted} operations`);
// Reverts can only reach back as far as retained oplog history —
// revertToSeq returns an error if the target sequence is older than the
// oldest retained entry (evicted by the retention limits).
// Browse collections programmatically
const collections = await server.getCollections();
const docs = await server.getDocuments('myapp', 'users', 50, 0);
OpLog Entry Structure
Each entry contains:
| Field | Type | Description |
|---|---|---|
seq |
number |
Monotonically increasing sequence number |
timestampMs |
number |
Unix timestamp in milliseconds |
op |
'insert' | 'update' | 'delete' |
Operation type |
db |
string |
Database name |
collection |
string |
Collection name |
documentId |
string |
Document _id as hex string |
document |
object | null |
New document state (null for deletes) |
previousDocument |
object | null |
Previous document state (null for inserts) |
✋ No-Op Write Detection
The engine detects document rewrites that change nothing and skips them entirely — no storage write, no WAL append, no index update, and no oplog entry. A skipped rewrite still counts as matched (matchedCount: 1) but reports modifiedCount: 0.
Only identical field values are skipped; top-level field order does not cause a write. Explicit changes to _updatedAt are persisted and returned just like changes to any other field. Applications decide when to advance their timestamps.
Counters are exposed through serverStatus:
const status = await db.command({ serverStatus: 1 });
console.log(status.writes);
// {
// updatesWritten: 42, // rewrites that reached storage, index, and oplog
// noopSkipped: {
// identical: 1337, // post-image equal to the stored document
// volatileOnly: 0, // retained metric; timestamp changes now write
// },
// }
API Reference
Find execution plans and pooled cursors
collection.find(filter).hint(index).sort(order).limit(count).explain('executionStats')
uses the serving query planner and reports actual document and index-entry reads.
queryPlanner inspects the plan without executing it; allPlansExecution also
executes the selected plan, with an empty trial-execution list because selection
is deterministic. Explain currently supports find outside transactions.
Queries can use a compound index's sort prefix after equality-constrained fields,
and scalar $in predicates on single-field indexes use bounded, deduplicated point
lookups. Unsupported point shapes retain ordinary predicate evaluation.
Driver cursors belong to their logical session and authenticated principal
generations, so getMore can use another connection in the pool. Session
retirement, expiry, and shutdown release their cursors. Reads made without a
logical session retain connection ownership and disconnect cleanup.
NoSqlDbServer
The core server class. Manages the Rust database engine and exposes connection details.
Constructor Options (INoSqlDbServerOptions)
binaryPath?: string selects one exact engine executable for this server. When
supplied, Smartrust validates a regular executable file and never searches
NOSQLDB_RUST_BINARY, npm packages, development directories or PATH, even if
the selected path is unusable. Missing, empty, malformed, non-file and
non-executable paths reject startup with RustBinaryLocatorError, code
ERR_RUST_BINARY_EXPLICIT_PATH_INVALID. Validation never changes the target's
permissions. Executable launch or readiness failures also fail startup without
selecting another engine. Omitting the option preserves normal discovery.
Use an absolute path to the caller-installed, verified artifact. Smartrust
anchors relative paths when the underlying bridge is constructed, without
normalizing away filesystem symlink traversal. A bare name is a relative file
path, not a PATH lookup. Options are instance-local; no environment variables
are changed. Installer verification and protected installation paths remain the
caller's responsibility.
import { NoSqlDbServer } from '@lossless.org/nosqldb';
const server = new NoSqlDbServer({
binaryPath: '/opt/spark/engines/nosqldb/rustdb_linux_amd64',
socketPath: '/run/spark/private/nosqldb.sock',
storage: 'file',
storagePath: '/var/lib/spark/nosqldb',
});
await server.start({ timeoutMs: 30_000 });
try {
const connectionUri = server.getConnectionUri();
// Pass connectionUri to SmartData for application persistence.
} finally {
await server.stop(); // Resolves after the owned engine has terminated.
}
The socket parent directory must already exist with the caller's desired access
permissions. RustDbBridge also accepts { binaryPath } for direct lifecycle use.
Native memory and file storage
The native MVCC engine is the sole serving engine on Linux and macOS.
storage: 'memory' (the default) uses ephemeral native storage;
storage: 'file' persists the same record model under storagePath.
binaryPath selects the executable; it is independent of storage selection.
fileStorageEngine, persistPath, persistIntervalMs and
auth.usersPath have been removed. Supplying them rejects at construction,
before filesystem or process side effects. Authentication belongs to the native
database and persists with file storage. Memory storage is ephemeral.
import { NoSqlDbServer } from '@lossless.org/nosqldb';
const server = new NoSqlDbServer({
binaryPath: '/opt/spark/engines/nosqldb/rustdb_linux_amd64',
socketPath: '/run/spark/private/nosqldb.sock',
storage: 'file',
storagePath: '/var/lib/spark/nosqldb',
auth: {
enabled: true,
scramIterations: 15000,
users: [{ database: 'app', username: 'app-owner',
password: bootstrapPassword, roles: ['readWrite'] }],
},
});
await server.start({ timeoutMs: 30_000 });
try {
const connectionUri = server.getConnectionUri();
// Connect SmartData with the app credentials after native ownership is acquired.
} finally {
await server.stop();
}
Supply bootstrapPassword from protected caller configuration. The work factor
above is an example, not a universal default. Reopen with the stored SCRAM work
factor; bootstrap users may be omitted after initialization. LocalNoSqlDb
accepts folderPath, socketPath, binaryPath and the same auth options.
Native query and maintenance efficiency
Bounded synchronous reads, including principal resolution, publication authority and query setup, share a retained generation lease. Filesystem ownership and generation bindings are validated before and after the batch; mutation, durability, cancellation and close checks remain enforced. No lease or resolved authorization is reused across requests or asynchronous suspensions.
Compound indexes combine leading equality fields, if any, with a range on the
next field. For example, { serverId: 1, timestamp: 1 } seeks directly into the
requested server's time interval. Inclusive/exclusive endpoints, descending
components, trailing fields and reverse pagination retain their normal
semantics. A range is built from $gt, $gte, $lt and $lte, and from
$ne: null or a $nin list holding null, which read only the non-null keys as
MongoDB's [MinKey, undefined) ∪ (null, MaxKey] bounds do: documents without
the field or with an explicit null are not examined. Ranges are built only on
complete, non-multikey indexes whose values all have a scalar order; other
indexes are not narrowed. The full predicate is still checked for every
selected candidate.
For a query whose filter references only _id and whose projection is exactly
{ _id: 1 }, the native primary index can supply the original BSON identity
without loading the document payload. Range endpoints, order, skip, limit and
cursor pagination retain their normal semantics. The engine verifies the index
and unique-owner values, the document frame's existence, and either matching
commit versions or a storage-owned SHA-256 certificate of the exact BSON _id.
Updates that keep the same identity leave the primary owner and its certificate
unchanged, avoiding an extra index rewrite. Numeric BSON width changes update
the certificate and primary identity atomically with the body. A covered read
does not checksum an unrequested document payload. Older records without a
usable identity proof use the normal bounded body read; the next actual write
adds the certificate.
Secondary-index projections and predicates requiring other fields retain normal
document reads. No application cache or configuration flag is required.
Concurrent wire insert, update, delete and findAndModify commands can share
one durability flush. Each command keeps its own commit marker and retry outcome.
Only the private write group sees provisional roots; readers and replies see
results after the final flush. The queue holds at most 16 jobs, adds no deliberate
delay, and ends each group after 16 jobs or 64 MiB of appended data (one admitted
command can cross that byte threshold). Authority, session and input-resource
lifetimes remain held through durability. History publication also waits for the
flush. DDL, transactions, index builds, imports and maintenance retain their
existing independently durable publication paths.
Storage compatibility: version 10 adds a 40-byte certified document-ordinal record alongside the legacy 8-byte ordinal. It reads existing stores directly; startup and reads perform no format rewrite. After version 10 writes certified records, use version 10 or later to reopen that store. Older binaries reject the new record representation. Keep a pre-upgrade backup if a binary downgrade is required. The K2 pages, commit markers and recovery protocol are unchanged.
Maintenance batches catalog work and parks completed reclamation and no-TTL sweeps until the committed position changes. Session cleanup skips its metadata scan until a session can expire. Compaction uses the persisted generation anchor as its baseline, so reopening an already-compacted root does not rewrite it. Compaction runs separately from TTL maintenance, observes shutdown cancellation, and does not restart a large copy because a short maintenance deadline elapsed. These maintenance optimizations preserve the existing generation and commit formats.
File startup admission
INoSqlDbServerOptions.fileStorageStartup and
ILocalNoSqlDbOptions.fileStorageStartup accept 'current-format-only'
(the default) or 'existing-only' for file storage.
The exported type is TNoSqlDbFileStorageStartup. Invalid values, including
'migrate', reject at construction. Selecting the policy with memory storage
also rejects.
Default startup accepts a fresh root or a current native root. Use
fileStorageStartup: 'existing-only' when the database must already exist:
missing paths, empty directories and identity-only roots without committed
initialization fail instead of creating an empty database. The native owner
enforces this policy; no TypeScript existence check or fallback can create a
new root. Older binaries reject the new option rather than silently ignoring it.
Rehearsal startup retains its separate current-format-only configuration.
Foreign, old, mixed and orphaned migration staging layouts are rejected without conversion. TypeScript performs no storage writes before starting the engine. Native format admission uses the owning detector; the exclusive native file-root lease is acquired before initialization, recovery, authentication or index preparation.
Only after await server.start() succeeds should consumers prepare SmartData
models or run application migrations. Start cancellation and deadlines cover the
native operation; failed-start cleanup confirms child exit, and stop() remains
retryable if termination cannot yet be confirmed. A second owner of the same
root is rejected without disturbing the winner. Callers must also exclude
manual edits and lifecycle changes outside NoSQLDB's ownership protocol.
No automatic format or auth-permission migration runs during startup.
Persisted-format conversions belong exclusively in the top-level
ts_migration/ folder and require their own explicit qualification. This
release contains no old-engine or authentication-schema conversion.
import { NoSqlDbServer } from '@lossless.org/nosqldb';
// TCP mode (default)
const server = new NoSqlDbServer({
port: 0, // Default: 27017; 0 requests an OS-assigned port
host: '127.0.0.1', // Default: 127.0.0.1
storage: 'memory', // 'memory' or 'file' (default: 'memory')
storagePath: './data', // Required when storage is 'file'
});
const startupController = new AbortController();
await server.start({
signal: startupController.signal,
timeoutMs: 30_000,
});
console.log(server.port); // Actual bound port while running
console.log(server.getConnectionUri()); // mongodb://127.0.0.1:<actual-port>
// Unix socket mode — no port conflicts!
const server = new NoSqlDbServer({
socketPath: '/tmp/nosqldb.sock',
storage: 'file',
storagePath: './data',
});
// Bounded in-memory oplog retention
const server = new NoSqlDbServer({
port: 27017,
oplog: {
maxEntries: 50000, // Default: 10000
maxBytes: 128 * 1024 * 1024, // Default: 64 MiB
},
});
// TLS transport for TCP mode
const tlsServer = new NoSqlDbServer({
port: 27017,
tls: {
enabled: true,
certPath: './certs/server.pem',
keyPath: './certs/server.key',
// caPath: './certs/client-ca.pem',
// requireClientCert: true, // Enables mTLS client certificate checks
},
});
// SCRAM-SHA-256 authentication
const secureServer = new NoSqlDbServer({
port: 27017,
auth: {
enabled: true,
users: [
{
username: 'root',
password: 'change-me',
database: 'admin',
roles: ['root'],
},
],
},
});
When auth.enabled is true, protected commands require successful SCRAM-SHA-256 authentication through the official MongoDB driver:
const client = new MongoClient('mongodb://root:change-me@127.0.0.1:27017/admin?authSource=admin', {
directConnection: true,
});
await client.connect();
TLS is available for TCP listeners. getConnectionUri() includes ?tls=true when TLS is enabled; pass the trusted CA to the MongoDB driver with tlsCAFile, ca, or secureContext.
Authentication verifies SCRAM credentials, denies unauthenticated commands, and enforces command-level built-in roles for supported operations. connectionStatus reports the authenticated users and roles for the current socket.
Supported built-in role names are root, read, readWrite, dbAdmin,
userAdmin, clusterMonitor, readAnyDatabase, readWriteAnyDatabase,
dbAdminAnyDatabase and userAdminAnyDatabase. Native auth records contain
derived SCRAM credentials, protected decoy material, the immutable work factor,
principal identities, generations and bounded grant replay state. Plaintext
passwords are not persisted. auth.scramIterations must match the stored
profile on every reopen. Historical or malformed auth records fail closed.
Every authenticated command resolves the current principal and generation. Password and role changes invalidate old sockets. Deleting and recreating a username creates a different principal; an old connection cannot inherit its authority. Auth changes and the data/control records they protect share native atomic publications.
Single-node transactions pin one committed root across namespaces and retain changed documents, rather than copying a database. The snapshot is pinned by the transaction's first statement. Supported reads and writes see the transaction snapshot plus its staged changes. Commit publishes atomically; abort discards staged changes.
Write conflicts follow MongoDB: the first statement to write a document or a unique index key wins, and the later writer is told at its own statement.
- A transaction statement that inserts, updates, replaces, deletes or upserts a
document, or claims or releases a unique index key, that another open
transaction has already written fails with
WriteConflict(112) andTransientTransactionError. So does a write to a document or key that was published after the transaction's snapshot. The failed statement aborts the transaction at once and releases everything it wrote: later statements andcommitTransactionreportNoSuchTransaction(251), also labelledTransientTransactionError. The transaction that wrote first keeps its documents and commits. The driver'swithTransactionretries the loser on a fresh snapshot, so a transaction that writes a hot document and then does more work makes progress against clients looping short transactions on it. - Reads never conflict: they see the snapshot, not another transaction's uncommitted writes. A statement that writes nothing (a filter that matches nothing in the snapshot, an update that leaves the document unchanged) does not conflict either.
- A write outside a transaction that meets an open transaction's uncommitted
write waits until that transaction commits or aborts, then runs on the new
state: after a commit it applies on top of the transaction's write, and a
unique key the transaction claimed becomes
DuplicateKey(11000). The wait is bounded bymaxTimeMS, which ends it withMaxTimeMSExpired(50) and nothing written, and holds no write worker, so the transaction can always finish. Writes to other documents are not delayed. A multi-document write outside a transaction stays all-or-nothing: it publishes nothing until it can publish every document. - A transaction open for longer than 60 seconds is aborted, as by MongoDB's
transaction reaper: its staged writes are discarded and its cursors, write
claims, transaction capacity and database gates are released. The server
checks idle sessions once a second; a statement still executing when the
limit passes is not interrupted, and the transaction aborts when that
statement has finished (MongoDB instead interrupts that statement with
TransactionExceededLifetimeLimitSeconds(290)). Its later statements,commitTransactionandabortTransactionreportNoSuchTransaction(251); statements andcommitTransactioncarryTransientTransactionError, sowithTransactionretries from the start. Before that abort, the next writer that needs one of its documents aborts it too rather than wait. The limit is fixed at MongoDB's defaulttransactionLifetimeLimitSecondsand is not configurable. Ending or expiring the session, and stopping the server, release a transaction's writes as well. - TTL expiry is not a contending writer. It leaves out the documents an open transaction has written, deletes the other expired documents of the index and moves past the held ones. A later pass over the index expires those still expired once the transaction has ended. It never waits for a transaction and never aborts one, whatever its age. MongoDB's TTL monitor instead retries a conflicting delete until the transaction ends and leaves aborting it to its transaction reaper.
- A key already visible in the transaction's snapshot is not a conflict:
inserting it fails the statement with
DuplicateKey(11000), as outside a transaction. A collection created inside a transaction has no stored identity until commit, so two transactions creating the same collection both succeed their statements and the second commit reportsWriteConflict(112), as on MongoDB 7.
Write intents live in memory only; no transaction survives a restart, so there is nothing to recover.
Commit/abort outcomes and successful retryable statement results survive file
storage restart. Duplicate commits replay their outcome; abort after commit
reports TransactionCommitted (256), commit after abort reports
NoSuchTransaction (251), and older transaction numbers report
TransactionTooOld (225). Unsupported transactional DDL, auth mutations and
$out/$merge reject before transaction admission or mutation.
Outside a transaction, createIndexes for already installed, identical indexes
completes without draining active transactions or publishing a new storage
position. A repeated physical create preserves NamespaceExists (48) under
the same shared admission. Both paths validate current authorization and catalog
options. New collections and changed index catalogs still drain transactions
and revalidate under exclusive admission before any publication.
Logical sessions survive socket disconnects. Expiry, endSessions and
killSessions drain active work and release cursors and namespace gates.
A commitTransaction or abortTransaction retried while its session is being
ended still replays the transaction's stored outcome, so a committed
transaction is never reported as aborted; a transaction that was still open
when its session ended publishes nothing and reports NoSuchTransaction (251).
Rejected new transaction envelopes do not consume session capacity or reset
existing sessions. See the native resource budgets below.
Durable Database Publication Holds
On supported Linux and macOS filesystems, file-backed NoSQLDB can keep a replaced or deleted database closed after the local mutation is durable and until a downstream coordinator confirms its own fsync. Check the explicit health contract before using this flow:
const health = await server.getHealth();
if (
health.publicationHoldVersion !== 1 ||
!health.publicationHoldSupported ||
health.publicationHoldRequiresExternalDrain
) {
throw new Error('NoSQLDB publication holds are unavailable');
}
Set holdPublication: true on an exact fenced importDatabase() or deleteDatabaseTenant() operation. The result contains a held resourceFence with a one-time publicationCapability. Treat that capability as a secret: do not log it or persist it outside protected control-plane state. After downstream state is durable, submit the exact receipt to commitDatabasePublication():
import type { INoSqlDbHeldPublicationReceipt } from '@lossless.org/nosqldb';
const held = await server.importDatabase({
databaseName: 'tenant_a',
source: snapshot,
username: 'tenant_a_user',
fence: {
version: 1,
scopeId: 'corestore-node-1.tenant-a',
token: 42,
mutationId: 'restore-42',
payloadSha256: controlPlanePayloadSha256,
},
holdPublication: true,
});
const receipt = held.resourceFence as INoSqlDbHeldPublicationReceipt;
const heldExport = await server.exportHeldDatabase({
databaseName: 'tenant_a',
resourceFence: receipt,
});
await verifyAndPersistDownstreamSnapshot(heldExport);
await persistAndFsyncDownstreamState(receipt);
const released = await server.commitDatabasePublication({
databaseName: 'tenant_a',
resourceFence: receipt,
});
The held barrier survives restart and blocks wire commands, transactions and index restoration for that database while unrelated databases remain available. exportHeldDatabase() is a bounded management-only read of one pinned committed catalog, auth and fence root. It requires the exact still-held receipt and capability, returns the same canonical Extended JSON shape as exportDatabase(), and does not release the barrier. Ordinary export and wire access remain closed; native TTL maintenance cannot expire held documents. A wrong, stale or already released receipt fails closed. Native physical compaction preserves the held records. Commit is exact and idempotent. A successful commit removes the raw capability from durable state and retains only protected verification material. Provider/root identity and bounded validation of native fencing records make copied, incomplete or corrupt allocation state fail closed. A higher fencing token compacts resolved older receipts, so long-lived coordinators do not exhaust receipt capacity.
Durable database publication holds and allocation fencing use native file storage on the supported Linux and macOS filesystems.
Coordinators that do not yet have their own durable record can call getDatabaseResourceFenceState({ databaseName }) before creating it. File-backed NoSQLDB returns the durable scope, highest token, current publication phase, and allocation lifecycle/identity when the name is allocation-managed, or null for an unfenced name. Startup first validates both directions of every allocation's fencing/auth binding. The inspection never returns mutation IDs, mutation payload digests, publication receipts, or publication capabilities. Treat any active phase, identity mismatch, corrupt state, unsupported storage, or unsafe token as a hard stop rather than creating coordinator state.
Authoritative Database Allocation
allocateDatabaseTenant() is the create-only control-plane API for permanently allocation-managed database names. It is available only when allocationFencingVersion === 1, allocationFencingSupported === true, and allocationFencingRequiresDrain === true. The request requires literal expectedAbsent: true, an INoSqlDbResourceFence, and the tenant database, username, and password. roles is optional and defaults to ['readWrite', 'dbAdmin']. Under native ownership and database/control admission, NoSQLDB proves catalog and principal absence, then commits the catalog, principal, complete auth allocation binding and fencing receipt at one position. It returns an INoSqlDbAllocateDatabaseTenantResult; its .allocation field is the durable INoSqlDbDatabaseAllocationIdentity. Exact fence replay returns the same allocation ID, generation, principal ID, and receipt digests.
Pass the returned allocation identity to every later ensureDatabaseTenant(), importDatabase(), and deleteDatabaseTenant() request for that name. Managed import and delete requests also require the exact allocation username; the identity alone is insufficient. Each distinct managed mutation requires a strictly newer fence token; the current token is accepted only for exact receipt replay or continuation, and older receipts become explicitly stale after advancement. Held publication receipts carry the allocation into commitDatabasePublication(). Managed requests without it and unmanaged requests with it fail closed. Deprovision drains affected runtime authority, then atomically retires the catalog, exact principal, grants and auth binding with a permanent deprovisioned fencing tombstone. A later allocation requires a newer fence token, increments generation, and receives a fresh allocation and principal identity. Wire data access is available only while active and only to the current allocation principal; database/user ownership-changing wire commands are rejected.
Allocation bindings are native records, excluded from logical database exports and never accepted from import payloads. Startup and stopped-storage inspection require the complete matching fence and auth binding even when no read grant was ever issued. Missing bindings are rejected without reconstruction. This includes allocated roots produced by experimental native versions that did not create the complete binding. This release provides no automatic conversion or downgrade path; never remove or edit private metadata to force admission.
Read-Only Tenant Attestation
attestDatabaseTenant() verifies that an existing database is exclusively owned by the exact username, role set, and candidate password. It returns only { matches: boolean }; it never returns a URI, credential, principal identity, or mismatch detail. Missing databases or users, conflicting ownership, role differences, allocation-principal drift, and password mismatch all return false through the same constant-work credential path.
Attestation pins one native snapshot under the database's read-only admission. Its auth, allocation and data observations share that committed root. Attestation does not refresh process caches or call tenant ensure, create, rotate, import, delete, fence repair, publication-epoch observation, or cache invalidation. Corrupt, held, deprovisioned, disabled, or busy provider state fails as an operational error instead of returning a match result.
const result = await server.attestDatabaseTenant({
databaseName: 'tenant_a',
username: 'tenant_a_user',
roles: ['readWrite'],
password: candidatePassword,
});
if (!result.matches) {
throw new Error('Existing tenant authority does not match');
}
Allocation-Bound Read Access Grants
Scratch diagnostics can use issueDatabaseAllocationReadAccessGrant() to create one auxiliary read principal for an exact active allocation. The capability is advertised only when databaseAllocationReadAccessGrantVersion === 1 and databaseAllocationReadAccessGrantSupported === true; this requires file storage, enabled authentication and ready allocation fencing. Native MVCC commits grant principals, authentication and allocation bindings atomically in the storage root. The caller supplies databaseName, a bounded grantId, a bounded unique username, password, and the complete INoSqlDbDatabaseAllocationIdentity. Roles and expiry are not caller-controlled. NoSQLDB always assigns exactly ['read'] and a fixed, non-renewing 12-minute expiry.
const grant = await server.issueDatabaseAllocationReadAccessGrant({
databaseName: allocation.databaseName,
grantId: 'scratch-diagnostic-2026-08-08',
username: 'scratch_diagnostic_reader',
password: generatedSecret,
allocation,
});
const diagnosticClient = new MongoClient(grant.mongodbUri!, {
directConnection: true,
});
await diagnosticClient.connect();
Exact issue replay validates the candidate password against the persisted SCRAM credential and returns the original principal generation, issuedAt, and expiresAt only while that grant remains active and unexpired; it never renews the grant. Expired or revoked grantId values are rejected. NoSQLDB permits at most one active read grant per allocation. Revoked and expired grantId values remain in a per-allocation anti-replay set capped at 1,024 entries, with no eviction while the allocation exists. Capacity exhaustion fails closed. Allocation deprovision uses a two-phase fail-closed sequence: any active grant is first durably retired and tombstoned, then the database is durably deleted, and finally the exact owner plus that allocation's replay state are removed in one auth rewrite. Interruption before completion leaves the grant retired rather than usable.
Native deprovision also durably retires an active grant before draining its transaction, then publishes database deletion, exact owner cleanup and its fencing receipt together. Bounded periodic cleanup retires expired grant principals and cancels their sessions and cursors.
Grant-specific rejections from the issue and revoke methods are normalized automatically as NoSqlDbAllocationReadAccessGrantError with stable EGRANT_INVALID_REQUEST, EGRANT_CREDENTIAL_MISMATCH, EGRANT_CONFLICT, EGRANT_REPLAY_CAPACITY, or EGRANT_STATE_UNAVAILABLE codes. The companion exports TNoSqlDbAllocationReadAccessGrantErrorCode, noSqlDbAllocationReadAccessGrantErrorCodes, and normalizeNoSqlDbAllocationReadAccessGrantError(error, code) support type-safe handling and explicit normalization of structured bridge errors. Allocation and resource-fence failures retain their existing NoSqlDbResourceFenceError codes. Callers should classify errors by these exported classes and codes rather than by message text.
Active allocation read grants never survive a NoSQLDB engine restart or a new attachment to the durable auth store. Startup durably retires and tombstones them before accepting work; diagnostics must issue a new grantId, username, and credential after restart.
Grant principals can read only their exact allocation database. Reads, collection/index metadata, and read-only transactions are supported. Writes, DDL, user/admin operations, writing aggregates ($out and $merge), cross-database targets, and allocation ownership changes are denied. Revocation, expiry cleanup, and allocation deprovision abort matching logical sessions and transactions, release retained database permits, and remove matching cursors. Stale sockets fail their next command.
revokeDatabaseAllocationReadAccessGrant() takes databaseName, grantId, and the complete allocation identity, returns { revoked: true } without a secret, and is idempotent. Revoke-before-issue records anti-replay state only while a read permit proves that exact allocation is current. A delayed revoke after deprovision returns success without recreating auth metadata.
Native exportDatabase() pins a committed database root while ordinary writes
continue. Catalog, auth and publication checks use the same root; DDL remains
excluded by the database gate. Callers can lower maxEncodedBytes,
maxCollections, maxDocuments and maxIndexes; the server enforces these
bounds while scanning. Results use canonical MongoDB Extended JSON, preserving
every BSON type and numeric width.
importDatabase() validates canonical or relaxed Extended JSON, builds a private
native catalog in bounded commits, then atomically publishes the complete catalog
with its fencing receipt. Incomplete staging is never visible to data readers.
Cancellation or restart retires unpublished staging through the owning native
logic. Index aliases, empty namespaces and validated system.views definitions
are preserved; view IDs are rebound when the database name changes. Keep exported
objects as JSON values rather than coercing BSON numeric wrappers into JavaScript
numbers. Imported indexes retain their validated source catalog specification,
including BSON field order and an absent v field in legacy exports.
These APIs transfer database contents; they do not convert storage files.
getDatabaseContentDigest() computes a bounded, database-name-independent SHA-256 over collection names, exact BSON document bytes, and persisted index specifications. Collection, document, and index enumeration order is normalized; BSON field order and compound-index key order remain significant. The physical system.views catalog is included, with only its already-validated deterministic database prefix removed from _id for hashing so a cross-name import retains the same digest. INoSqlDbGetDatabaseContentDigestInput accepts databaseName and optional INoSqlDbDatabaseContentDigestLimits fields maxScannedBsonBytes, maxCollections, maxDocuments, and maxIndexes. Callers may lower the server ceilings of 96 MiB scanned BSON, 10,000 collections, 1,000,000 documents, and 100,000 indexes. The INoSqlDbDatabaseContentDigest result reports format smartdb.database.content-digest.v1, algorithm sha256, the lowercase digest, and exact scan counters.
NoSqlDbServer.start() and the lifecycle-critical health, fence-state, digest, export, import, publication-commit, tenant-allocation, tenant-attestation, tenant-ensure, tenant-delete, read-grant issue, and read-grant revoke methods accept optional INoSqlDbManagementOperationOptions with signal?: AbortSignal and timeoutMs?: number. timeoutMs, when provided, must be a positive safe integer no greater than 2,147,483,647. Startup applies both cancellation and one deadline across sidecar spawn, native admission, recovery and database readiness. Cancellation and deadlines are fail-stop once the sidecar is owned: NoSQLDB attempts to terminate the Rust engine before rejecting. Once termination is confirmed, the operation cannot continue after the caller releases ownership. If termination itself fails, the rejection retains bridge ownership and the service owner must retry stop() until it succeeds. After cancellation or deadline termination, restart NoSQLDB before accepting more traffic.
const controller = new AbortController();
const exported = await server.exportDatabase(
{ databaseName: 'myapp' },
{ signal: controller.signal, timeoutMs: 5 * 60_000 },
);
Basic user management commands are available for authenticated users with root or userAdmin privileges:
await client.db('admin').command({
createUser: 'reader',
pwd: 'readpass',
roles: [{ role: 'read', db: 'myapp' }],
});
await client.db('admin').command({ usersInfo: 'reader' });
Methods & Properties
| Method / Property | Type | Description |
|---|---|---|
start(options?) |
Promise<void> |
Start under one optional cancellation/deadline budget; join partial sidecars before rejection or retain ownership for stop() retry if termination fails |
stop() |
Promise<void> |
Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup |
getConnectionUri() |
string |
Get the active mongodb:// URI; before start and after stop, port 0 remains unresolved |
running |
boolean |
Whether the server is currently running |
port |
number |
Actual bound port while running; otherwise the configured port (TCP mode) |
host |
string |
Configured host (TCP mode) |
socketPath |
string | undefined |
Socket path (socket mode) |
processId |
number | undefined |
Operating-system pid of the spawned Rust engine while it runs as a child process (for crash and kill tests) |
getMetrics() |
Promise<INoSqlDbMetrics> |
Server metrics (db/collection counts, sessions, transactions, auth, uptime) |
getOpLog(params?) |
Promise<IOpLogResult> |
Query oplog entries with optional filters |
getOpLogStats() |
Promise<IOpLogStats> |
Aggregate oplog statistics |
revertToSeq(seq, dryRun?) |
Promise<IRevertResult> |
Revert to a specific oplog sequence (must be within retained oplog history) |
getCollections(db?) |
Promise<ICollectionInfo[]> |
List all collections with counts |
getDocuments(db, coll, limit?, skip?) |
Promise<IDocumentsResult> |
Browse documents with pagination |
getHealth(options?) |
Promise<INoSqlDbHealth> |
Read readiness and explicit publication-hold capability fields with optional fail-stop cancellation/deadline ownership |
allocateDatabaseTenant(params, options?) |
Promise<INoSqlDbAllocateDatabaseTenantResult> |
Authoritatively allocate an absent file-backed tenant and return its durable allocation identity; supports fail-stop cancellation and deadlines |
attestDatabaseTenant(params, options?) |
Promise<INoSqlDbAttestDatabaseTenantResult> |
Verify exact existing tenant ownership, roles, and candidate credential without mutating provider, auth, fence, or runtime cache state |
issueDatabaseAllocationReadAccessGrant(params, options?) |
Promise<INoSqlDbIssueDatabaseAllocationReadAccessGrantResult> |
Issue or exactly replay one fixed 12-minute read grant bound to the complete active allocation identity |
revokeDatabaseAllocationReadAccessGrant(params, options?) |
Promise<INoSqlDbRevokeDatabaseAllocationReadAccessGrantResult> |
Idempotently revoke/tombstone one exact allocation read grant and invalidate its live runtime state |
ensureDatabaseTenant(params, options?) |
Promise<INoSqlDbEnsureDatabaseTenantResult> |
Idempotently ensure an exact fenced tenant; supports fail-stop cancellation and deadlines |
getDatabaseResourceFenceState(params, options?) |
Promise<INoSqlDbDatabaseResourceFenceState | null> |
Safely inspect a file-backed database fence high-water mark and publication phase without exposing receipts or capability material; supports fail-stop cancellation and deadlines |
getDatabaseContentDigest(params, options?) |
Promise<INoSqlDbDatabaseContentDigest> |
Compute a bounded, database-name-independent digest over exact BSON documents and persisted index specifications; supports fail-stop cancellation and deadlines |
exportDatabase(params, options?) |
Promise<INoSqlDbDatabaseExport> |
Export one database as lossless canonical MongoDB Extended JSON; supports fail-stop cancellation and deadlines |
exportHeldDatabase(params, options?) |
Promise<INoSqlDbDatabaseExport> |
Export one exact still-held database under its publication receipt without opening wire access or releasing the hold; accepts ordinary export limits, cancellation and deadlines |
importDatabase(params, options?) |
Promise<INoSqlDbImportDatabaseResult> |
Durably replace one database from canonical or relaxed MongoDB Extended JSON, optionally leaving publication held; supports fail-stop cancellation and deadlines |
deleteDatabaseTenant(params, options?) |
Promise<INoSqlDbDeleteDatabaseTenantResult> |
Durably delete an exact tenant database/user, optionally leaving publication held; supports fail-stop cancellation and deadlines |
commitDatabasePublication(params, options?) |
Promise<TNoSqlDbCommitDatabasePublicationResult> |
Idempotently release an exact held publication receipt; supports fail-stop cancellation and deadlines |
NoSqlDbStorageRehearsal
NoSqlDbStorageRehearsal.createDestination(input, options?) creates an isolated,
authenticated copy of an initialized, stopped file-storage root. It verifies
the source's existing native owner, provider and authentication bindings while
holding its existing outer and native file-root leases continuously.
The source is read-only throughout: no initialization, recovery, migration,
permission repair, journal retirement or identity editing occurs there. Successful
creation leaves it independently restartable at its original path.
import { NoSqlDbServer, NoSqlDbStorageRehearsal } from '@lossless.org/nosqldb';
const binaryPath = '/opt/corestore/engines/nosqldb/rustdb_linux_amd64';
const operation = new NoSqlDbStorageRehearsal({ binaryPath });
const request = {
sourceFolderPath: '/var/lib/corestore/authoritative',
destinationFolderPath: '/var/lib/corestore/rehearsals/upgrade-1',
operationId: 'corestore-upgrade-1',
};
const controller = new AbortController();
try {
// Corestore has stopped the source and retains its maintenance exclusion.
const receipt = await operation.createDestination(request, {
timeoutMs: 30_000,
signal: controller.signal,
});
const candidate = new NoSqlDbServer({
binaryPath,
storage: 'file',
storagePath: receipt.destinationFolderPath,
fileStorageStartup: 'current-format-only',
storageRehearsalId: receipt.operationId,
socketPath: '/run/corestore/private/rehearsal.sock',
auth: {
enabled: true,
scramIterations: receipt.scramIterations,
},
});
try {
await candidate.start({ timeoutMs: 30_000 });
const uri = candidate.getConnectionUri();
// Connect SmartData with the preserved credentials. Prepare its models,
// then run Corestore's versioned ts_migration modules against this URI.
// Keep external services and application side effects isolated in Corestore.
} finally {
await candidate.stop();
}
// Retain the typed receipt through Corestore's normal persistence API.
} finally {
await operation.stop();
}
The destination's parent and socket parent must already exist, be owned by the
effective user and have mode 0700. The socket must be absent and outside the
storage root. Source and destination paths must be absolute, canonical, distinct,
non-nested paths on the same supported local filesystem/device. The destination
must be absent on the first attempt. Neither request nor receipt contains a
password. binaryPath selects exactly the caller-verified executable through
Smartrust; omission retains normal engine discovery without environment changes.
The public types are INoSqlDbStorageRehearsalOptions,
INoSqlDbStorageRehearsalInput, INoSqlDbStorageRehearsalReceipt and
INoSqlDbStorageRehearsalPreservation. An optional
expectedSourceProviderRootId pins a previously attested source provider ID;
NoSQLDB always verifies the source's stored bindings even when this is omitted.
Consumers do not inspect private identity files. Optional
limits: { maximumEntries, maximumBytes } can lower the source scan limits.
The supported storage profile is native-mvcc-authenticated-v1; creation
evidence uses smartdb.storage-rehearsal.receipt.v2. Rehearsal accepts current
native authentication records only. The removed authMigration option and
schema-2 worker conversion are not accepted.
| State | Rehearsal contract |
|---|---|
| Contents and catalogs | Preserve exact BSON documents, indexes and aliases, empty namespaces, and internal databases including admin, local and config. |
| Authentication | Preserve usernames, roles, salts, password verifiers, work factor, principal identities, generations and retained grant replay history. |
| Fencing and holds | Preserve allocations, retired history, counters and exact receipt records. Held databases remain held; a rehearsal cannot release their production capabilities. |
| Sessions and outcomes | Preserve durable retry and transaction outcomes, including retained and retiring session records. |
| Native history | Preserve the source group/timeline and committed records. The destination adds one committed provider-binding record with isolated purpose; it never resets authority counters. |
Active source owners, rehearsal-as-source, old or mixed layouts, unknown entries, unfinished import or fencing operations, active read grants, unfinished or quarantined indexes, invalid catalogs, corrupt auth/outcomes and storage requiring recovery are rejected before publication. Source symlinks, hardlinks, nested mounts, special objects, unsafe owners/modes and identity changes are rejected. Creation supports the qualified Linux local-filesystem set and writable local macOS APFS with native ownership and atomic publication. Other filesystems and operating systems are rejected before destination initialization.
Defaults and hard limits are 100,000 filesystem entries, 1 TiB of file bytes, depth 16, 4095-byte paths and 64 MiB of retained inventory metadata. The owning native catalog, index, auth, outcome and fencing validators also enforce their record and semantic work budgets. Exhausted bounds reject without truncation or reset. LSNs and fencing high-water marks in the receipt are decimal strings, preserving their complete unsigned 64-bit range. These are operation bounds, not total serving document limits.
Copying and hashing stream 64 KiB chunks. Complete source/destination validation compares the preserved record digest as well as physical identity and contents; this is not a constant-time snapshot or reflink.
storageRehearsalId?: string is supported by both INoSqlDbServerOptions and
ILocalNoSqlDbOptions. It requires the exact recorded operation ID, file storage,
fileStorageStartup: 'current-format-only', the recorded auth work factor,
enabled authentication, no bootstrap users, and a private explicit Unix socket.
LocalNoSqlDb also forwards auth?: INoSqlDbAuthOptions. Ordinary startup rejects
a rehearsal root. Native startup verifies the completion seal and new physical
provider binding and preserved native auth profile before recovery or auth initialization. Health
reports storagePurpose: 'rehearsal' and storageRehearsalId; production fencing,
allocation and publication capabilities are disabled. Authenticated ordinary
SmartData reads, writes and transactions remain available on unheld databases.
Corestore owns application/outbound-service isolation. There is no promotion
API: changing startup flags cannot turn this destination into production authority.
One instance admits one operation at a time; independent instances can select
different executables and sources. Native leases exclude active source owners
and conflicting copies. timeoutMs defaults to one hour and is capped at 24 hours;
the budget includes engine lookup/spawn and the native operation. Abort, deadline
and stop() terminate the owned sidecar; methods settle after confirmed exit.
Cleanup can outlast the deadline. If termination cannot be confirmed, the instance
retains its bridge and stop() remains retryable. Native work also checks parent
process death. Source stability relies on the native lease plus the caller's
maintenance exclusion from manual edits and restarts between attempts.
Publication uses a private sibling operation workspace, a bounded durable journal, a distinct native destination lease and an atomic exclusive rename. The renamed root cannot start until its completion seal is durable. After interruption, retry the identical request, including paths, operation ID, expected provider ID and limits, using the same NoSQLDB release or a compatible newer release. A retry may use a different deadline. Keep the source stopped and unchanged until an unfinished copy completes; do not edit, remove or move either root or the operation workspace. Completed retries return the same creation receipt even after legitimate rehearsal writes or source restart. They verify the destination's identity and seal and re-establish publication durability; they do not attest to its later contents.
NoSqlDbStorageRehearsalError has fixed, secret-free messages and no raw nested
cause. Codes are INVALID_REQUEST, UNSUPPORTED, BUSY, NOT_FOUND,
DESTINATION_CONFLICT, IDENTITY_MISMATCH, STATE_CORRUPT, SOURCE_CHANGED,
CAPACITY, CANCELLED, IO, RECOVERY_REQUIRED and BINARY_UNAVAILABLE.
Uncertain command completion or cleanup reports RECOVERY_REQUIRED; an error
does not imply an absent destination. Exact retry reconciles interrupted copies,
renames and receipt writes without replacing unrelated paths. A changed source
or tampered destination fails closed.
The receipt identifies both paths/providers, the destination device/inode, request
and receipt hashes, storage profile, public auth work factor, preservation counts
and explicit productionAuthority: false / promotionSupported: false. It exposes
no usernames, principal IDs, per-file auth hashes, verifiers or publication secrets.
It is integrity-checked creation evidence, not an installer signature. Source
verification compares complete bytes, object identities, owners, link counts,
modes, modification times and change times; ordinary read access times may change.
Matching Linux and macOS amd64/arm64 engines and their checksum/provenance files ship in the
same published NoSQLDB package's dist_rust/ directory; see the engine artifact
section below.
LocalNoSqlDb
Zero-config wrapper around NoSqlDbServer. Uses Unix sockets and file-based persistence.
Constructor Options (ILocalNoSqlDbOptions)
import { LocalNoSqlDb } from '@lossless.org/nosqldb';
const db = new LocalNoSqlDb({
folderPath: './data', // Required: data storage directory
socketPath: '/tmp/custom.sock', // Optional: custom socket (default: auto-generated)
binaryPath: '/opt/spark/engines/nosqldb/rustdb_linux_amd64', // Optional: exact engine
fileStorageStartup: 'current-format-only', // Optional; also the default
});
binaryPath has the same strict selection contract as NoSqlDbServer and is
passed through unchanged on every start. Supply an absolute path because the
underlying server and bridge are constructed by LocalNoSqlDb.start().
Methods & Properties
| Method / Property | Type | Description |
|---|---|---|
start() |
Promise<ILocalNoSqlDbConnectionInfo> |
Start and return connection info |
stop() |
Promise<void> |
Stop the server |
getConnectionInfo() |
ILocalNoSqlDbConnectionInfo |
Get current connection info |
getConnectionUri() |
string |
Get the connection URI |
getServer() |
NoSqlDbServer |
Access the underlying server |
running |
boolean |
Whether the server is running |
LocalNoSqlDb.relocateStoppedStorageRoot(input, options?) |
Promise<ILocalNoSqlDbStoppedStorageRootRelocationReceipt> |
Atomically relocate one eligible stopped Linux or macOS file-storage root and retain a source receipt |
LocalNoSqlDb.alignStoppedStorageRoot(input, options?) |
Promise<TLocalNoSqlDbStoppedStorageRootAlignmentResult> |
Adopt an eligible stopped copy as an independent provider |
LocalNoSqlDb.abortStoppedStorageRootAlignment(input, options?) |
Promise<ILocalNoSqlDbStoppedStorageRootAlignmentAbortResult> |
Abort an exact incomplete alignment append |
LocalNoSqlDb.inspectOfflineStringValue(input, options?) |
Promise<TLocalNoSqlDbOfflineStringValueInspectionResult> |
Read one exact top-level string value from stopped file storage without starting or mutating the engine |
LocalNoSqlDb.inspectOfflinePhysicalNamespaces(input, options?) |
Promise<ILocalNoSqlDbOfflinePhysicalNamespaceInspectionResult> |
Validate stopped physical file storage and return only deterministic database and collection names |
Stopped Storage-Root Relocation
relocateStoppedStorageRoot() moves an eligible stopped file-storage root without starting RustDb, binding a listener, running a TypeScript migration, initializing storage or auth, or performing WAL recovery. It supports Linux on the existing qualified local-filesystem set and macOS on writable local APFS volumes with ownership, persistent object identities, flock, and atomic exclusive/swap rename support. The source root, source parent, and destination parent must be trusted descriptor-safe objects on the same filesystem. The supplied source and destination must be absolute and resolve to distinct, non-root, non-nested physical paths, and the destination must be absent. macOS also rejects case-folded, Unicode-normalized, and ancestry aliases before creating relocation state. The trusted parents may differ.
import { LocalNoSqlDb } from '@lossless.org/nosqldb';
const receipt = await LocalNoSqlDb.relocateStoppedStorageRoot({
sourceFolderPath: '/var/lib/myapp/harness-controller/nosqldb',
destinationFolderPath: '/var/lib/myapp/hcon/nosqldb',
relocationId: 'move-2026-08-17-1',
}, {
timeoutMs: 30_000,
});
// Start only the destination. The source is now the retained receipt file.
const db = new LocalNoSqlDb({ folderPath: receipt.destinationFolderPath });
await db.start();
The input must be a plain object with exactly the own enumerable data fields sourceFolderPath, destinationFolderPath, and relocationId. Each supplied path must already be absolute, is normalized before sidecar creation, is limited to 1 through 4095 UTF-8 bytes, and cannot contain control characters. Longer destination paths are supported within that bound. relocationId is a 1 through 256-byte ASCII identifier beginning with an alphanumeric character and otherwise using only alphanumerics, ., _, :, @, or -. Management timeoutMs and signal use the same validation and one-deadline behavior as other NoSQLDB management operations. The native operation also checks its monotonic deadline and parent-process lifetime during admission and publication.
The returned exact receipt has this shape:
interface ILocalNoSqlDbStoppedStorageRootRelocationReceipt {
format: 'smartdb.storage-root-relocation.receipt.v1';
version: 1;
relocationId: string;
sourceFolderPath: string;
destinationFolderPath: string;
providerRootId: string;
storageRootDevice: string; // canonical decimal u64
storageRootInode: string; // canonical decimal u64
receiptSha256: string;
sourceReceiptRetained: true;
}
On success, the destination directory is the original storage-root inode and the source path is an exact mode-0600 regular-file receipt. NoSQLDB does not remove that source receipt. The operation appends one native commit that changes only the fencing profile's physical path binding. All other record bytes and versions are preserved, including documents, indexes, empty namespaces, auth verifiers, principal identities, transaction outcomes, fencing history, holds and counters. The native group and timeline, provider identity, and original fencing receipt domain remain unchanged. The native commit position advances exactly once, including after retries.
Relocation supports stopped current native production roots with or without authentication. It never initializes missing ownership or storage metadata. Rehearsal destinations, legacy or mixed layouts, active read-access grants, unfinished fencing or allocation operations, building or quarantined indexes, symlinks, unknown entries, unsafe modes/ownership/link counts, exceeded inspection bounds, active owners and ambiguous identities are rejected explicitly. Admission uses the same owning catalog, auth, index, outcome and fencing validators as other stopped native operations. This operation moves the existing authority; it does not promote a rehearsal copy or duplicate production authority.
The operation durably records one immutable native append plan and reserves the receipt before changing the path binding. It retains exclusive ownership through the append, atomic directory/receipt exchange and confirmed journal cleanup. If a call returns RECOVERY_REQUIRED, stop startup attempts and retry the exact normalized source path, destination path and relocationId with the same native-capable NoSQLDB version. Startup rejects any relocation-workspace occupancy and never mutates or recovers it. Do not delete, rename, edit or replace either path between attempts. Native relocation does not accept unfinished journals from the removed Bitcask engine.
An exact completed replay returns the same receipt only while the destination remains the current root. A later successful relocation leaves another receipt at that destination path and supersedes the earlier replay topology; retry the later operation instead. Manual copy or rename fails provider path/inode fencing. alignStoppedStorageRoot can adopt an eligible copy as an independent fork under the stricter policy below; it does not move production authority.
Failures are exposed as LocalNoSqlDbStorageRootRelocationError with a code-only union: INVALID_REQUEST, UNSUPPORTED, BUSY, NOT_FOUND, DESTINATION_CONFLICT, IDENTITY_MISMATCH, STATE_CORRUPT, SOURCE_CHANGED, CAPACITY, CANCELLED, IO, or RECOVERY_REQUIRED. Errors and receipts contain no credential or document material. Transport ambiguity after dispatch is RECOVERY_REQUIRED; exact retry is the recovery action. An explicit native cancellation or I/O error can also leave private recovery state, which the identical request can finish after the sidecar has confirmed exit.
Stopped storage-root alignment
Never start a copied or restored root before alignment. Ordinary serving startup may recover a torn tail before rejecting its foreign binding. Stop the source, inspect it for pins, copy it while it remains stopped, and align the copy before its first start. Pins cannot prove that an unverified backup is complete; a file truncated exactly at a commit boundary can still be internally consistent.
Version 1 requires current production file storage, a clean committed tail, no
generation newer than the manifest, no auth users, and no fencing, allocation,
publication-hold, grant or import-stage authority. The complete catalog, auth,
index, transaction-outcome and fencing audits must pass. Rehearsals, memory
storage, unknown files (including .DS_Store/AppleDouble), symlinks, hard links
and unsafe ownership or modes are refused. The operation never repairs them.
The attestation covers every copy of the lineage, not just the local root.
import { LocalNoSqlDb } from '@lossless.org/nosqldb';
// Run against the source after stopping it; retain these pins through the copy.
const source = await LocalNoSqlDb.inspectStoppedStorageRoot({ folderPath: '/srv/source/db' });
if (source.status !== 'audited' || !source.alignmentEligible) throw new Error('Source is ineligible');
const request = {
folderPath: '/srv/destination/db',
receiptPath: '/srv/destination/adoption.json',
alignmentId: 'laptop-move-1',
identityPolicy: 'fork' as const,
attestLineageNeverFenced: true as const,
expect: { groupId: source.groupId, timelineId: source.timelineId, lsn: source.lsn,
recordsSha256: source.recordsSha256, providerRootId: source.providerRootId },
};
// /srv/destination is an existing mode-0700 directory owned by the service user.
const preview = await LocalNoSqlDb.alignStoppedStorageRoot({ ...request, dryRun: true });
const result = await LocalNoSqlDb.alignStoppedStorageRoot(request, { timeoutMs: 60_000 });
All paths must be canonical absolute paths. receiptPath is required, outside
the root on the same filesystem, with an existing private parent. An unrelated
existing file is never overwritten. Use the relocation identifier rules for
alignmentId. Supply either expect from the stopped source or explicit
acknowledgeUnverifiedCopy: true when no source pins survive, never both.
binaryPath, timeoutMs and signal select the engine and bound the operation.
The following archive recipe was qualified from macOS arm64 APFS to Linux x86_64 ext4 and XFS. Run as the service user, with the source stopped throughout, and retain its inspection pins separately. Start with a new private destination parent; preserve the private file/directory modes and do not include filesystem metadata sidecars. On macOS use the exact canonical case and Unicode spelling; alternate spellings are refused rather than treated as portable aliases.
# macOS: /absolute/source-parent/db is stopped and inspected.
COPYFILE_DISABLE=1 tar --no-xattrs -czf /absolute/db-copy.tar.gz -C /absolute/source-parent db
# After transferring the archive to Linux, as the destination service user:
mkdir -m 700 /absolute/destination-parent
tar --no-same-owner -xzf /absolute/db-copy.tar.gz -C /absolute/destination-parent
# Run alignStoppedStorageRoot with the retained source pins before any start.
A dry run returns eligible with inspected evidence. An eligible root already
bound to its location returns already-bound with bindingChange: 'none'.
Neither creates a workspace or receipt. Alignment appends exactly one fresh
schema-1 fencing profile: a new providerRootId, live physical binding and
LSN+1. Every other record's bytes and version remain unchanged. groupId and
timelineId remain lineage identifiers; they are not the new provider identity.
The immutable receipt records both provider IDs, both LSNs, source/aligned full
digests, preserved-record digest and request/receipt SHA-256 commitments. Retain it.
A pre-reserved receipt proves completion only together with its matching target
profile and lineage. Exact retries verify that evidence and return the same
receipt, including after subsequent writes with the same target profile.
Alignment failures throw NoSqlDbStoppedOperationError with a code-only code.
On RECOVERY_REQUIRED, keep the exact request and all files unchanged and retry
alignment with the same capable engine. Its private workspace excludes ordinary
startup and retains the exact append plan through crashes and partial writes.
Do not delete or edit the workspace or receipt. BUSY means an owner still holds
the root; IDENTITY_MISMATCH or SOURCE_CHANGED requires investigating changed
pins or files. UNSUPPORTED indicates an ineligible root or older engine;
stopped inspection provides bounded eligibility reasons. Other code-only errors
include INVALID_REQUEST, NOT_FOUND, DESTINATION_CONFLICT, STATE_CORRUPT,
CAPACITY, CANCELLED and IO.
abortStoppedStorageRootAlignment(request, options?) accepts the identical
request and only rolls back an exact incomplete planned append. A complete
append must finish alignment. If permissions changed after interruption, restore
the required ownership and modes first. Abort permits changed timestamps while
still checking original inodes, the full inventory, original bytes and append
prefix. It durably records abort, truncates/fsyncs, removes its exact reserved
receipt and cleans up. Retry abort after an interrupted abort; alignment cannot
override that decision. An absent workspace returns not-pending.
Older 8.0.2 engines reject pending workspaces with “startup rejects existing file-provider bookkeeping”. Finish or abort with the newer engine; deleting the workspace is not a fix. A v8-produced fixture remains readable by v8 after this one-profile append. This does not allow downgrading v10-created catalogs. Qualification results and study corrections are recorded in the repository's alignment design note.
Stopped Storage-Root Inspection
LocalNoSqlDb.inspectStoppedStorageRoot({ folderPath }, { timeoutMs?, signal?, binaryPath? })
audits an existing stopped source or copy without initializing or repairing it.
Use a canonical absolute path. The outer and native ownership locks remain held
through the complete record-owner audit. A running engine returns BUSY.
An audited result includes the recorded provider identity, live binding state,
lineage (groupId/timelineId), decimal-string LSN and counts, full-record and
profile-excluded SHA-256 digests, generation inventory and alignment eligibility.
It counts allocation replays as well as fences, receipts, holds and allocations.
The audit accepts no active grants or import stages. Auth users or authority
history make a fully audited root ineligible for independent-fork alignment.
An unavailable result contains bounded advisory reasons and no trusted logical
identity, digests or partial counts. It indicates physical-admission or owner-audit
failure; it is not evidence that a copy is safe. Reading may affect filesystem
access times, but never changes data, modes, ownership or modification timestamps.
All stopped inspection and relocation methods accept binaryPath to select an
exact executable. Unsupported management methods in older engines report
UNSUPPORTED; interrupted dispatched mutations still require recovery. Health
also exposes providerRootId and the current decimal-string lsn, and non-null
resource fence summaries expose providerRootId. These fields are optional in
the TypeScript interfaces for older engine binaries.
Offline String-Value Inspection
LocalNoSqlDb.inspectOfflineStringValue(input, options?) reads one string from a
stopped current native file root on supported Linux filesystems or macOS APFS.
It starts only a disposable management sidecar, acquires the existing native
leases read-only, and pins the committed root. It never starts a listener,
initializes storage, repairs tails, replays writes or compacts the source.
import { LocalNoSqlDb } from '@lossless.org/nosqldb';
const result = await LocalNoSqlDb.inspectOfflineStringValue({
folderPath: '/var/lib/myapp/nosqldb',
databaseName: 'myapp',
collectionName: 'MetadataDoc',
match: { field: 'key', value: 'schemaVersion' },
valueField: 'value',
limits: {
maximumEntries: 100_000,
maximumTotalFileBytes: 1024 ** 4,
maximumRecords: 1_000_000,
maximumRecordBytes: 16 * 1024 * 1024,
maximumResultBytes: 1024 * 1024,
},
}, { timeoutMs: 5000 });
if (result.status === 'found') console.log(result.value);
The result is exactly { status: 'not-found' } or
{ status: 'found', value: string }. No other document fields cross IPC.
Multiple matches, non-string results, corrupted storage, changed source state,
unsafe objects, missing owner metadata and exhausted limits reject the operation.
Offline Physical Namespace Inspection
LocalNoSqlDb.inspectOfflinePhysicalNamespaces(input, options?) validates and
lists the stopped native database/collection catalog. Empty namespaces are
retained. It validates native document/count/index relationships and compares
complete bounded filesystem observations before returning; no partial result is
published. Documents, index definitions, IDs and values stay inside the sidecar.
const namespaces = await LocalNoSqlDb.inspectOfflinePhysicalNamespaces({
folderPath: '/var/lib/myapp/nosqldb',
limits: {
maximumEntries: 100_000,
maximumDatabases: 1_000_000,
maximumCollections: 1_000_000,
maximumIndexes: 1_000_000,
maximumTotalFileBytes: 1024 ** 4,
maximumRecordsPerCollection: 1_000_000_000,
maximumRecordBytes: 16 * 1024 * 1024,
maximumResultBytes: 16 * 1024 * 1024,
},
}, { timeoutMs: 30_000 });
// { schemaVersion: 1, databases: [{ name: 'myapp', collections: ['MetadataDoc'] }] }
These required limits are validated before sidecar creation:
| Limit | Supported range |
|---|---|
maximumEntries |
1–100,000 filesystem entries |
maximumTotalFileBytes |
1 byte–1 TiB |
maximumRecords / maximumRecordsPerCollection |
1–1,000,000,000 |
maximumRecordBytes |
5 bytes–16 MiB |
maximumResultBytes |
1 byte–16 MiB for a value; 32 bytes–16 MiB for namespaces |
maximumDatabases, maximumCollections, maximumIndexes |
1–1,000,000 each for namespace inspection |
| Complete encoded request | At most 2 MiB |
timeoutMs |
1–2,147,483,647 milliseconds |
All numeric values must be safe integers. Paths are limited to 4095 UTF-8 bytes
before and after absolute resolution; namespace and field names to 255 bytes.
The string match is at most 1 MiB and must fit maximumRecordBytes.
Removed per-Bitcask-file limits are rejected.
One optional deadline and AbortSignal cover lookup/spawn, native inspection and delivery. Cancellation kills and reaps the disposable sidecar before rejection. An active database owner blocks inspection. The caller must retain continuous lifecycle exclusion through acceptance or handoff of the result: the native lease covers inspection, and cannot prevent a restart after the sidecar exits.
NoSqlDbDebugServer
Web-based debug dashboard served via @api.global/typedserver. Import from the debugserver subpath:
import { NoSqlDbDebugServer } from '@lossless.org/nosqldb/debugserver';
const debugServer = new NoSqlDbDebugServer(server, { port: 4000 });
await debugServer.start();
// Dashboard at http://localhost:4000
await debugServer.stop();
The UI is bundled as base64-encoded content (via @git.zone/tsbundle) and served from memory — no static file directory needed.
NoSqlDbDebugUi (Web Component)
For embedding the debug UI directly into your own web application, import the <nosqldb-debugui> web component:
import { NoSqlDbDebugUi } from '@lossless.org/nosqldb/debugui';
// In your HTML/lit template:
// <nosqldb-debugui .server=${myNoSqlDbServer}></nosqldb-debugui>
//
// Or in HTTP mode (when served by NoSqlDbDebugServer):
// <nosqldb-debugui apiBaseUrl=""></nosqldb-debugui>
Supported Operations
NoSQLDB supports the core operations through the wire protocol. Use the standard mongodb driver — these all work:
CRUD
// Insert
await collection.insertOne({ name: 'Bob' });
await collection.insertMany([{ a: 1 }, { a: 2 }]);
// Find
const doc = await collection.findOne({ name: 'Bob' });
const docs = await collection.find({ age: { $gte: 18 } }).toArray();
// Update
await collection.updateOne({ name: 'Bob' }, { $set: { age: 25 } });
await collection.updateMany({ active: false }, { $set: { archived: true } });
// Delete
await collection.deleteOne({ name: 'Bob' });
await collection.deleteMany({ archived: true });
// Replace
await collection.replaceOne({ _id: id }, { name: 'New Bob', age: 30 });
// Find and Modify
await collection.findOneAndUpdate({ name: 'Bob' }, { $inc: { visits: 1 } }, { returnDocument: 'after' });
await collection.findOneAndDelete({ expired: true });
await collection.findOneAndReplace({ _id: id }, { name: 'Replaced' }, { returnDocument: 'after' });
Query Operators
// Comparison
{ age: { $eq: 25 } } { age: { $ne: 25 } }
{ age: { $gt: 18 } } { age: { $lt: 65 } }
{ age: { $gte: 18 } } { age: { $lte: 65 } }
{ status: { $in: ['active', 'pending'] } }
{ status: { $nin: ['deleted'] } }
// Logical
{ $and: [{ age: { $gte: 18 } }, { active: true }] }
{ $or: [{ status: 'active' }, { admin: true }] }
{ $not: { status: 'deleted' } }
// Element
{ email: { $exists: true } }
{ type: { $type: 'string' } } { value: { $type: ['int', 'long'] } }
// Array
{ tags: { $all: ['mongodb', 'database'] } }
{ scores: { $elemMatch: { $gte: 80, $lt: 90 } } }
{ labels: { $not: { $elemMatch: { $nin: ['linux', 'gpu'] } } } }
{ items: { $elemMatch: { sku: 'a', qty: { $gt: 2 } } } }
{ items: { $all: [{ $elemMatch: { sku: 'a' } }, { $elemMatch: { qty: 0 } }] } }
{ tags: { $size: 3 } }
// Regex
{ name: { $regex: /^Al/i } }
// Aggregation expressions
{ owner: 'o', $expr: { $lte: [{ $add: ['$retainedBytes', '$reservedBytes', size] }, max] } }
{ $expr: { $gt: ['$leaseExpiresAt', '$$NOW'] } }
$type takes a type alias ('string', 'number', …), a numeric BSON type
code or an array of them, and matches an array field through its elements as
well as 'array' itself.
$elemMatch has MongoDB's two forms. An operand whose first key is a query
operator applies its operators ($eq, $ne, $gt, $gte, $lt, $lte,
$in, $nin, $exists, $type, $regex, $size, $mod, $all, $not,
$elemMatch) to each element as a whole; any other operand is a query over
each embedded document, and a nested array is matched as a document keyed by
index. A dotted path tests every array it reaches on its own. $expr inside
$elemMatch is refused with BadValue (2), as on MongoDB.
$expr evaluates an aggregation expression per document in find, count,
distinct, update, delete, findAndModify and a $match stage, and
matches when the result is truthy. It accepts the operators listed under
Aggregation Pipeline. Evaluation is charged against
the same work budget as every other predicate, and an operator the engine does
not implement is refused by name with TypeMismatch (code 14) before any
document is read. $$NOW is one instant per command, as in MongoDB: the
engine reads the clock once when the command starts, and the command's filter,
every document of a pipeline update, every aggregation stage, $lookup and
$unionWith sub-pipeline, and every getMore of its cursor see that instant.
Filters outside a command, such as a partial index filter, have no $$NOW.
Update Operators
{ $set: { name: 'New Name' } }
{ $unset: { tempField: '' } }
{ $inc: { count: 1 } }
{ $mul: { price: 1.1 } }
{ $min: { low: 50 } } { $max: { high: 100 } }
{ $push: { tags: 'new' } } { $pull: { tags: 'old' } }
{ $addToSet: { tags: 'unique' } }
{ $pop: { queue: 1 } } // Remove last
{ $pop: { queue: -1 } } // Remove first
{ $rename: { old: 'new' } }
{ $currentDate: { lastModified: true } }
Aggregation-pipeline updates are supported wherever the MongoDB driver accepts
them (updateOne, updateMany, findOneAndUpdate, and upserts): the update
may be an array of $addFields, $set, $project, $unset, $replaceRoot
or $replaceWith stages, and any other stage is refused by name. The pipeline
must produce exactly one document and cannot change _id.
Aggregation Pipeline
const results = await collection.aggregate([
{ $match: { status: 'active' } },
{ $group: { _id: '$category', total: { $sum: '$amount' } } },
{ $sort: { total: -1 } },
{ $limit: 10 },
{ $project: { category: '$_id', total: 1, _id: 0 } },
]).toArray();
Supported stages: $match, $project, $group, $sort, $limit, $skip, $unwind, $lookup, $addFields, $count, $facet, $replaceRoot, $set, $unionWith, $out, $merge
Supported expression operators:
| Group | Operators |
|---|---|
| Comparison | $eq, $ne, $gt, $gte, $lt, $lte, $cmp |
| Arithmetic | $add, $subtract, $multiply, $divide |
| Logical | $and, $or, $not |
| Conditional and variables | $cond (array or if/then/else form), $ifNull (two or more operands), $let |
| Type and arrays | $isNumber, $type, $in, $size, $arrayElemAt |
| Dates | $dateAdd |
| Collections | $map, $objectToArray, $setEquals |
| Literals | $literal |
plus $field paths, $$variable references including $$ROOT and $$NOW,
and documents or arrays of those. Every other operator is refused by name with
TypeMismatch (code 14) before any document is read; the engine never treats
an operator it cannot execute as a literal value.
Expressions follow MongoDB's value semantics. A field that is absent is
missing, which is not null: $type answers 'missing', $ifNull
replaces both, and comparisons order values as MongoDB does (MinKey, missing,
null, numbers, strings, objects, arrays, binary data, ObjectIds, booleans,
dates, timestamps, regular expressions, MaxKey), so { $eq: ['$absent', null] }
is false. Numbers compare by value across their wire types. A path through an
array maps over its elements and always answers an array: '$mailbox.path'
over one looked-up document is ['INBOX'], so $arrayElemAt and $size
apply to it. Arithmetic answers null when an operand is null or missing;
an int result that overflows widens to a long and a long to a double, a double
operand makes the result a double, and $divide always answers a double.
$add accepts one date and answers a date; date minus date is milliseconds.
Runtime errors keep MongoDB 7's codes: a non-numeric operand is
TypeMismatch (14), can't $divide by zero is BadValue (2), a date that
leaves the 64-bit range is Overflow (15), and the array and date operators
answer MongoDB's location codes (a second date in $add 16612, $in without
an array 40081, $size without an array 17124, $arrayElemAt 28689–28691,
$dateAdd 5166403, 5166405, 5166406 and 5439013, a non-string time zone
40517, an unknown unit FailedToParse (9)). Decimal128 operands in arithmetic
are refused with TypeMismatch (14) — decimal arithmetic is not
implemented. $and, $or, $cond and $ifNull
evaluate, and charge, only the operands MongoDB evaluates, so a guard keeps an
invalid branch from failing the statement. $dateAdd computes in UTC with the
units millisecond to year, clamping month, quarter and year to the
end of the target month; a timezone other than UTC is refused by name.
Within a document expression a member that is missing is still written as
null, where MongoDB omits the member.
$lookup supports the equality form and a bounded correlated pipeline form with
let, an optional $match (any filter, $expr included), and an optional
following $limit. A let variable that reads an absent local field is bound
to the missing value, as in MongoDB. Correlated string equality predicates
($expr with $eq, optionally under $and) can use a full-key or subset
foreign equality index to reduce candidates when available. Candidates are
paged, the complete expression remains authoritative, and $limit counts only
exact expression matches. Other BSON value shapes use the bounded scan path.
Mixed localField/foreignField plus pipeline lookups and other inner stages
are rejected explicitly. Complete filter trees are validated before data is
read, and query nesting plus aggregation work are bounded.
A leading $match narrows the pipeline's source load instead of filtering
after it: only matching documents are materialized, using an equality index when
one serves the filter and a bounded scan otherwise. Filter shapes the query
matcher cannot pre-compile fall back to the unnarrowed load, so results are
unchanged. The materialization limit therefore applies to the matched set, which
is what lets countDocuments(filter) run against collections larger than it.
The scalar members of a $in or $nin list are looked up in a set built when
the filter is validated, so a document costs the size of its value, not the
length of the list: countDocuments({ _id: { $in: ids } }) answers for tens of
thousands of ids. Equality is unchanged: numbers of every type are equal by
value (1, NumberLong(1), 1.0 and NumberDecimal("1.00")), an array field
matches through its elements or as a whole, and a missing field matches a list
that holds null. Documents, arrays and regular expressions in a list are
compared one by one and charged as such. A regular expression in a list is
still compared as a value and not yet applied to strings, which differs from
MongoDB, where it matches the strings it describes; that difference is a known
open defect.
Group accumulators: $sum, $avg, $min, $max, $first, $last, $push, $addToSet, $count
$group._id accepts a literal (null, a number, a string), a $field
reference, or a document of expressions: { _id: { owner: '$owner', repo: '$repo' } } groups by the evaluated pair, one group per distinct composite
key. Inside a composite _id, a member whose field is absent evaluates to
null and stays part of the key, where MongoDB omits the member instead.
Accumulator operands accept the same expression forms. A field reference
resolves per document, and a missing field is not a value: $sum, $avg,
$min, $max, $push and $addToSet skip it, while $first and $last
record null for it. Every field beside _id must hold an accumulator
document naming exactly one accumulator, such as { $sum: 1 }, and an operator
outside the two lists above is refused with TypeMismatch (code 14) when the
stage is parsed.
Indexes
await collection.createIndex({ email: 1 }, { unique: true });
await collection.createIndex({ name: 1, age: -1 }); // compound
await collection.createIndex({ field: 1 }, { sparse: true });
const indexes = await collection.listIndexes().toArray();
await collection.dropIndex('email_1');
await collection.dropIndexes(); // drop all except _id
🛡️ Unique indexes are enforced at the engine level. Duplicate values are rejected with a
DuplicateKeyerror (code 11000) before the document is written to disk — oninsertOne,updateOne,findAndModify, and upserts. Index definitions and entries are committed with native storage and survive restart.
NoSQLDB supports ascending and descending index keys with name, unique,
sparse, partialFilterExpression and expireAfterSeconds options. It
accepts background as a validated no-op and index version v: 2.
Unsupported options, including collation and hidden, are rejected before
any index in the request is created. A legacy ns field is accepted only when
it names the target namespace. With ignoreUnknownIndexOptions: true,
createIndexes removes the fields MongoDB 7 does not know as index options
(and plugin-specific fields such as weights or bits on an ascending or
descending key) before validation, as MongoDB does; options MongoDB knows but
NoSQLDB does not implement are still rejected. Native maintenance expires eligible BSON
dates and date arrays in bounded batches, preserving publication holds and
active snapshots.
Partial indexes. An index with a partialFilterExpression holds only the
documents that match the filter, and a unique partial index enforces
uniqueness among those documents only:
await jobs.createIndex(
{ activeClaimKey: 1 },
{ unique: true, partialFilterExpression: { activeClaimKey: { $exists: true } } },
);
await pulls.createIndex(
{ org: 1, repo: 1, sourceRef: 1, targetRef: 1 },
{ unique: true, partialFilterExpression: { state: 'open' } },
);
The filter accepts MongoDB's subset: equality (field: value or $eq),
$exists: true, $gt, $gte, $lt, $lte, $type, $in, $and and
$or, nested at most four expression levels deep. Any other expression —
$exists: false, $ne, $nin, $not, $nor, regular expressions,
$elemMatch, $size, $expr, and a regular expression inside $in — and a
filter combined with sparse are refused with CannotCreateIndex (code 67);
a malformed filter is BadValue (2) and a non-document is TypeMismatch (14).
An update that moves a document into the filter claims its key, one that moves
it out releases it, and transactions claim keys the same way, so a racing
claim is a WriteConflict (112) with TransientTransactionError.
listIndexes, canonical export and import report the filter exactly as it was
created. The filter identifies the index: the same name with a different
filter is IndexKeySpecsConflict (86); the same key with an equivalent filter
under another name is IndexOptionsConflict (85); a different filter over the
same key under another name is a separate index. Equivalence treats implicit
equality and $eq, the order of conjuncts and $or branches, numeric wire
types and $in duplicates as MongoDB's normalised expressions do. The query
planner never selects a partial index on its own; a hint naming one reads
only its members, as MongoDB's hinted scan does. A partial TTL index expires
only its members. Engines older than this release refuse a storage root that
holds a partial index instead of enforcing its uniqueness over every
document.
find() and collection aggregate() accept hint as an index name (including
_id_) or an exact ascending/descending key pattern. Key-pattern order matters;
if several indexes share a key pattern, use a name. The selected active scalar
index supplies every candidate, using safe full-key or leading-prefix equality
bounds, or a full scan of that index. The complete predicate still runs, and explicit sorting,
projection, skip, and limit retain their normal semantics. Index traversal uses
bounded pages from a pinned committed root across client requests. Unsorted results have no guaranteed BSON value order.
await collection.createIndex({ organizationId: 1, userId: 1 }, { name: 'organization_user' });
const page = await collection.find({ organizationId: 'org-1' }, {
hint: 'organization_user', sort: { userId: 1, _id: 1 }, skip: 20, limit: 10,
}).toArray();
const count = await collection.countDocuments({ organizationId: 'org-1' }, {
hint: 'organization_user',
});
Unknown or quarantined indexes, malformed/ambiguous patterns, $natural, and
hints on missing collections or views fail explicitly. Forced multikey traversal
is unsupported and rejects; unhinted array queries keep their existing scan
behavior. Sparse hints intentionally exclude documents absent from that index,
including for empty filters and counts. A hint is never silently replaced by a
collection scan or another index.
Hinted transaction reads construct the selected index from the existing bounded
transaction snapshot plus buffered writes; they never fetch candidate documents
from live storage. Read-only aggregate source pipelines also use that transaction
view. Foreign-collection stages ($lookup, $unionWith) and writing stages
($out, $merge), including stages inside $facet, reject within transactions.
Nontransaction execution uses the streaming and bounded-resource paths below.
Scalable query execution
Collections have no fixed total-document ceiling on the nontransaction find,
count, distinct, update, delete, and findAndModify paths. Result cursors
read bounded pages instead of retaining the whole result. Multi-document writes
visit each source document once, including when updates move indexed values.
Index publication stages changed keys instead of copying the complete collection
index engine for each write.
Nontransaction aggregation pipelines composed of $match, $project, $unset,
$set, $addFields, $replaceRoot, $replaceWith, $skip, $limit, $sort,
$group, $count, $unwind, $lookup, $unionWith, and $facet also operate
beyond 10,000 input or output documents.
Counts use exact collection metadata where applicable, otherwise stream matching
records. A limit bounds the work needed to find the requested number of matches.
Without a limit, exact counts continue to EOF. Hints, deadlines and empty-result
shapes remain supported; signed BSON count overflow and resource exhaustion
reject explicitly. Exact counts never substitute estimates, and transaction
counts retain the snapshot admission limits described below.
Grouping accumulates one group at a time after an external sort over the group
key; only keys with no field reference and no operator stream directly into a
single group. Composite document keys sort and group like any other key. Group
key and accumulator semantics follow the supported expression operators listed
above; this does not add the complete MongoDB aggregation language.
Unwind retains one input while expanding arrays across batches. Lookup scans
bounded foreign pages or indexed candidates, then assembles one byte-bounded
result document. Unindexed joins can still require repeated foreign scans.
Facet branches replay a shared input buffer or temporary spool; their combined
result must fit the BSON document budget.
Scalar ordered indexes can satisfy a sort prefix after equality fields, including reverse traversal and mixed compound directions. This path currently requires distinct full index keys. Equal requested sort keys follow the remaining index columns; include a unique field in the sort when tie order matters. Arrays, unsupported BSON index types, and duplicate full index keys use the general sort executor. Numeric comparison and scalar equality indexes share exact Int32, Int64, Double, and Decimal128 ordering, including large integers, signed zero, and NaNs.
Small sorted limits retain top-k candidates. Larger sorts and groups use bounded
runs in unnamed temporary files, with automatic cleanup on exhaustion, cursor
closure, cancellation, errors, and process exit. Configure the shared resources
through NoSqlDbServer:
const server = new NoSqlDbServer({
storage: 'file',
storagePath: '/path/to/database',
query: {
sortBufferBytes: 4 * 1024 * 1024,
maxSortSpillBytes: 4 * 1024 * 1024 * 1024,
maxConcurrentSorts: 4,
},
});
sortBufferBytes is an encoded-input target, not an exact allocator heap limit;
a single BSON document may exceed the target. The spill budget includes retained
cursor files, facet input spools, and simultaneous merge input/output. Exhausting resource admission
returns an explicit error instead of truncating results. Cursor continuations
share a 256 MiB accounting budget. One continuation admits at most 160 MiB plus
4 KiB, including pinned query state, changed-document overlays and bounded
large-index-key candidates. Wire batches and pending document pages retain their
16 MiB limits. distinct
results and individual group documents must fit their BSON response/document
byte budgets. The 10,000-operation wire batch limit still applies to each request;
the driver can split larger input batches.
Native streaming reads pin a committed root across cursor pages. Later writes do
not change that snapshot. Current authorization and publication ownership are
revalidated before a continuation is served.
Ownership checks, namespace gates, byte budgets, and maxTimeMS remain active
through getMore and cleanup.
Transactions use pinned roots and changed-document overlays; the total database
size is not a transaction snapshot limit. Complete write sets remain bounded by
transaction and native commit byte admission. $out and $merge have no
10,000-document output ceiling; their atomic output is subject to the same write
budgets and rejects before publication if those budgets are exceeded. Native
database import uses private staging for larger supported exports, then publishes
the catalog and receipt atomically. Existing old-engine storage is rejected
without conversion.
Comparing performance with MongoDB
The repository includes benchmark/run.ts, which starts fresh,
loopback-only NoSQLDB and original MongoDB processes with isolated temporary
storage. Supply an original mongod executable; the harness does not download
one or use a MongoDB-compatible replacement as the reference server.
NOSQLDB_RUST_BINARY="$PWD/rust/target/release/rustdb" \
MONGODB_BINARY=/path/to/original/mongod \
tsx benchmark/run.ts --engine both --sizes 1000,10001,100000 \
--concurrency 1,16 --repetitions 3 --samples 250 --warmup 50 \
> .nogit/scaling-comparison.jsonl
Run on Linux after building the current binary and stopping competing benchmark
or build work. Both engines use the same driver and data, w:1, j:true, local
reads, snapshot transactions, disabled retryable writes, and one connection per
worker. MongoDB runs as a single-voter replica set. Repetitions alternate engine
order. --id-type string covers string identifiers; --payload-bytes changes
record size. Full scans and transactions use the separate --scan-samples count
(default 3) because they are substantially more expensive.
JSON-lines output records binary hashes, host/version metadata, correctness failures, seed/index time, latency percentiles, throughput, process RSS and peak RSS, CPU ticks, and physical read/write bytes. Temporary database directories are retained for inspection and their paths are recorded. Failed operations remain visible; they must not be compared as successful fast queries. Small sample counts and a shared development host are diagnostic evidence, not a general claim of superiority over MongoDB. The recorded comparison includes both measured improvements and remaining gaps.
Database & Admin
await db.listCollections().toArray();
await db.createCollection('new');
await db.dropCollection('old');
await db.dropDatabase();
await db.stats();
const admin = client.db().admin();
await admin.listDatabases();
await admin.ping();
await admin.serverStatus();
NoSQLDB creates ordinary collections, collections with document validators (see below) and one deliberately restricted view form. Capped collections, collation, time-series collections, clustered indexes, encrypted fields, and all general view pipelines remain unsupported and are rejected before the database or namespace is created.
create accepts an idIndex only when it describes the primary index every
collection gets: key { _id: 1 }, name _id_, optionally v: 2 and an ns
naming the new collection. Any other idIndex is InvalidOptions (72).
Importing from MongoDB. mongorestore (MongoDB Database Tools 100.x)
restores a mongodump of ordinary collections with its default flags: the
documents, validators and supported indexes (unique, compound, sparse,
partial and TTL). It sends this idIndex with every create and
createIndexes with each index's ns and ignoreUnknownIndexOptions: true,
and it builds several collections' indexes in parallel, which queue on the
database (see "Database waits" below). Exclude the admin and config
namespaces (--nsExclude 'admin.*' --nsExclude 'config.*') and create users
through NoSQLDB's authentication options. Collections or indexes that use
unsupported features (collation, text, geospatial, hashed or wildcard
indexes, capped or time-series collections) fail the restore with the
refusing command's error instead of being restored differently.
Collection Validators
create and collMod accept MongoDB's validator, validationLevel
(strict, moderate, off) and validationAction (error, warn), and
listCollections reports them under options exactly as set:
await db.createCollection('ffb_ci_v2_runners', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['revokedAt'],
properties: { revokedAt: { bsonType: ['date', 'null'] } },
},
},
validationLevel: 'strict',
validationAction: 'error',
});
await db.command({ collMod: 'ffb_ci_v2_runners', validationLevel: 'moderate' });
A validator is a query document. Its top level may hold one $jsonSchema;
every other clause is an ordinary query predicate evaluated by the query
engine, $expr with the command's $$NOW included. The $jsonSchema subset
is bsonType (every BSON alias and number), type (object, array,
number, boolean, string, null), required, properties,
additionalProperties (boolean), minProperties, maxProperties, enum,
minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength,
maxLength, pattern, items (one schema), minItems, maxItems, title
and description, with MongoDB's semantics: a keyword applies only to values
of its kind and not at all when bsonType/type excludes that kind, enum
compares numbers by value and embedded documents regardless of field order,
NaN fails minimum and maximum, and lengths count code points. Everything
else is refused when the validator is set, never ignored: patternProperties,
additionalItems, uniqueItems, dependencies, allOf, anyOf, oneOf,
not, multipleOf, encrypt, encryptMetadata, items as an array and
additionalProperties as a schema answer FailedToParse (9) $jsonSchema keyword '…' is not currently supported, as MongoDB answers for $ref,
format, default, definitions, id and $schema; $jsonSchema below the
top level answers BadValue (2). Malformed validators answer MongoDB's codes
(2, 9, 14, Location51091 for an invalid pattern). pattern uses the same
regular-expression dialect as $regex; constructs only PCRE supports are
refused as invalid.
Every insert and update is validated: inserts, upserts, update operators,
replacements, pipeline updates, findAndModify, bulk writes, $merge and
$out output, inside and outside transactions. strict validates every
written document; moderate skips updates to documents that did not satisfy
the validator before the update; off validates nothing; a no-op update and a
delete are never validated. A refused document answers
DocumentValidationFailure (121), errmsg: "Document failed validation", and
MongoDB's errInfo: { failingDocumentId, details } — as a write error of
insert/update, keeping ordered and unordered batch semantics, and as the
command error of findAndModify and aggregate. A refused statement aborts
its transaction. details has MongoDB's shape for every $jsonSchema keyword
above and for $and, $or, $nor, $not, $eq, $ne, $gt, $gte,
$lt, $lte, $exists, $type, $in, $nin, $regex, $size, $all,
$elemMatch, $mod and $expr; other operators report only operatorName
and specifiedAs. validationAction: 'warn' writes the document and logs a
Document would fail validation warning with the namespace and errInfo.
bypassDocumentValidation (a boolean or number) on insert, update,
findAndModify and writing aggregate skips validation; with authentication
it requires dbAdmin on the written database, as collMod does — readWrite
alone is refused with Unauthorized (13), as in MongoDB's built-in roles.
collMod changes only the options it names; validator: {} removes the
validator; a null option is absent; collMod without options answers
{ ok: 1 }. A missing collection answers NamespaceNotFound (26), a view or
system.* collection InvalidOptions (72), MongoDB's other collMod options
(index, viewOn, pipeline, expireAfterSeconds, timeseries,
changeStreamPreAndPostImages, dryRun, cappedSize, cappedMax, …)
InvalidOptions (72) by name, and unknown fields IDLUnknownField (40415).
Options persist with the collection, survive restarts and renameCollection,
and $out keeps its target's options. The encoded options are limited to
60 KiB (MongoDB has no separate limit); larger ones answer InvalidOptions
(72). Canonical export writes a collection's options beside its documents and
indexes only when it has options, import restores them, and the database
content digest includes them only then, so digests of collections without
options are unchanged. Engines up to 10.6.1 refuse a storage root or an export
holding collection options instead of silently not enforcing them.
Differences from MongoDB 7: create on an existing collection answers
NamespaceExists (48) even when the options are identical (MongoDB answers
ok); a validator together with viewOn is refused (MongoDB ignores it); an
invalid pattern at the schema root is refused (MongoDB never compiles it
there);
command errors say Document failed validation without MongoDB's
Plan executor error during … prefix; the warn log carries errInfo, not
the whole document.
Restricted Deny-All Views
The only accepted view definition is the exact deny-all pipeline below. This is intended for compatibility barriers such as renaming a legacy source and leaving its old namespace present but permanently empty:
await db.collection('secrets').rename('secrets_v2');
await db.createCollection('secrets', {
viewOn: 'secrets_v2',
pipeline: [{ $match: { $expr: { $eq: [1, 0] } } }],
});
Both viewOn and pipeline are required. The pipeline must contain exactly one
$match stage with exactly $expr: { $eq: [1, 0] }; empty pipelines, numeric or
structural variants, additional stages, and additional collection-definition
options are rejected with InvalidOptions (code 72) before mutation. Command
metadata such as $db, lsid, comment, and API-version fields remains
accepted. viewOn must identify an existing same-database collection or
restricted view at creation time. General MongoDB views are not supported.
The logical view has no physical collection. find, count, and distinct
return empty results. aggregate starts with an empty source and still applies
the client pipeline; $lookup and $unionWith resolve the view as empty.
Transactions first reading a current view record an explicit empty snapshot; a
transaction that captured a populated collection before it is renamed and
replaced by a view keeps that original snapshot for find, count, and
distinct until commit conflict handling. Inserts, updates, deletes,
findAndModify, indexes, $out, $merge, rename, and other data mutations fail
with CommandNotSupportedOnView (code 166). dropCollection is the supported
DDL exception for removing a logical restricted view.
Definitions are persisted in the physical system.views catalog. As in MongoDB,
listCollections lists system.views as a physical storage collection and the
logical namespace as type view with { viewOn, pipeline }, read-only metadata,
and no idIndex. system.views is reserved from direct wire reads,
writes, index operations, create, drop, rename, and aggregation output. Whole
database drop and trusted database export/import/digest operations handle it as
physical catalog data. Export never materializes a view; cross-database import
remaps the exact catalog _id database prefix. Startup validates databases whose
publication lease can be acquired before listener bind; a publication-held
database is validated on its first permit after release. Import validates before
mutation. Malformed, oversized, noncanonical, duplicate, reserved, or colliding
catalog state fails closed.
system.views was not interpreted by earlier SmartDB releases. This reservation
is a major-version compatibility boundary: before upgrading, operators must use
the older release to rename or remove any unrelated physical system.views
collection. Once upgraded, direct catalog rename or drop is intentionally
rejected with code 166.
Bulk Operations
const result = await collection.bulkWrite([
{ insertOne: { document: { name: 'Bulk1' } } },
{ updateOne: { filter: { name: 'X' }, update: { $set: { bulk: true } } } },
{ deleteOne: { filter: { name: 'Expired' } } },
]);
Count & Distinct
const count = await collection.countDocuments({ status: 'active' });
const estimated = await collection.estimatedDocumentCount();
const names = await collection.distinct('name');
The count command answers n as a 32-bit integer when the count fits and as
a 64-bit integer above that, as MongoDB does.
Wire Protocol Commands
| Category | Commands |
|---|---|
| Handshake | hello, isMaster, ismaster |
| CRUD | find, insert, update, delete, findAndModify, getMore, killCursors |
| Aggregation | aggregate, count, distinct |
| Indexes | createIndexes, dropIndexes, listIndexes |
| Sessions | startSession, endSessions, killSessions |
| Transactions | startTransaction, commitTransaction, abortTransaction through driver sessions |
| Admin | ping, buildInfo/buildinfo, listDatabases, listCollections, drop, dropDatabase, create, collMod (validation options), serverStatus, dbStats, collStats, connectionStatus, renameCollection |
Advertises MongoDB wire protocol versions 0–21. The documented command surface is tested with the official mongodb Node.js driver version 7.5.x.
OP_MSG parsing rejects unknown required flags, invalid CRC-32C checksums,
duplicate BSON keys, noncanonical array indexes, malformed section boundaries,
multiple body sections, duplicate or colliding document-sequence identifiers,
and missing or non-string $db fields. Early command-envelope validation then
accepts document sequences only as insert.documents, update.updates, or
delete.deletes; empty, oversized, extra, mismatched, and body-colliding forms
reject before authentication or command resources are acquired. All three accepted
forms are consumed as the corresponding bulk command argument. Unknown optional
flags are ignored. A message is limited to 48,000,000 bytes, each BSON document to
16,777,216 bytes, and BSON nesting to 100 levels. One message may contain at most
128 sections, 10,000 document-sequence documents, 100,000 BSON elements, and
100,000 BSON document or array containers. The advertised maxWriteBatchSize is
therefore 10,000.
The moreToCome request flag is accepted only for the exact endSessions or
killSessions cleanup envelope: an admin command body containing only the
session array, writeConcern: { w: 0 }, and $db: "admin", with no document
sequence. It executes without a transport response and leaves the connection
reusable. Every other moreToCome envelope closes before command routing or
response-owned resource creation.
Raw receive buffers and parsed requests are charged separately against a process-wide 256 MiB encoded-byte admission budget. A partial frame is charged by its incrementally reserved receive capacity, not by the length in its prefix. Receive leases retain their high-water charge until the buffer is dropped or successfully compacted, and every allocation growth is reconciled before another socket read. Large unread pipelines remain charged and are compacted only at geometric utilization thresholds; the budget is an encoded-input bound rather than a claim about exact allocator heap usage.
Identifying the Engine
NoSQLDB speaks the MongoDB wire protocol, so it must also state plainly what it
is. Every hello, ismaster and isMaster reply carries a self-contained
nosqldb document, and so does buildInfo/buildinfo:
{
"product": "NoSQLDB",
"vendor": "lossless.org",
"version": "<the released package version>",
"storageEngine": "native",
"mongodbCompatibility": "7.0.0",
"minWireVersion": 0,
"maxWireVersion": 21
}
version is the released @lossless.org/nosqldb version the running binary was
built from: the Rust build compiles it in from the package manifest, so the
answer belongs to the shipped artifact and cannot drift from it.
mongodbCompatibility is the MongoDB release whose wire protocol and command
semantics this engine implements — a compatibility statement, never a claim to
be that build.
The handshake carries the identity unconditionally, before authentication, so a
client can derive its backend from the connection it already has. buildInfo is
available to every authenticated connection, including a readWrite-only
tenant user and an allocation read grant; it requires no clusterMonitor role
and creates no session state. buildInfo answers from build metadata alone and
therefore still answers on a fenced or closed runtime, while hello fails
closed. It answers in MongoDB's shape so existing tooling keeps working:
{
"version": "7.0.0",
"versionArray": [7, 0, 0, 0],
"gitVersion": "<source commit, or \"unknown\" when the build had no Git metadata>",
"modules": [],
"javascriptEngine": "none",
"bits": 64,
"debug": false,
"maxBsonObjectSize": 16777216,
"storageEngines": ["native"],
"nosqldb": { "...": "the identity above" },
"ok": 1
}
buildInfo.version states the implemented compatibility level, exactly like
serverStatus.version; the engine's own version is in nosqldb.version. A
commit is never fabricated: gitVersion is either the exact source commit of
the build or the literal unknown. serverStatus keeps every field it already
reported, including version and process: "rustdb", and now reports the same
nosqldb identity beside them — but it remains a clusterMonitor command, so
least-privilege connections use hello or buildInfo.
Read and Write Concerns and Wire Deadlines
NoSQLDB serves every database from exactly one voting node and states its concern profile in those terms instead of ignoring or blanket-rejecting the fields:
- Write concern. Every acknowledged write is fsynced to the write-ahead log
and the data file before the response is sent, so
w: 1,w: "majority"(the majority of one voter is that voter), andj: trueare all satisfied by the same durable commit.w: 0is acknowledged the same way whenever a response is expected; the exactwriteConcern: { w: 0 }endSessionsandkillSessionsenvelopes still run without a transport response.wtimeoutbounds a replication wait that does not exist on one node and has no additional effect.w: NforN > 1fails withUnsatisfiableWriteConcern(code 100), tag-set modes fail withUnknownReplWriteConcern(code 79), andj: truefails withBadValue(code 2) on the in-memory storage backend, which does not journal. Write concern is accepted only on commands that write (including DDL, user management,commitTransaction, andabortTransaction) and is rejected withInvalidOptions(code 72) inside a multi-statement transaction, where it belongs on the commit. - Read concern.
local,available,majority, andlinearizableall observe the durably committed state, because nothing is acknowledged before it is durable and reads never observe uncommitted transaction buffers.snapshotis accepted only on the first statement of a multi-document transaction and is served by the database-wide transaction snapshot described below. Read concern is accepted onfind,count,distinct, andaggregateonly, andafterClusterTime,atClusterTime, andafterOpTimeare rejected withInvalidOptionsbecause a single node issues no cluster time. - Deadlines.
maxTimeMSattaches a cooperative deadline to the command. It is checked at admission, during namespace-lock and maintenance-gate waits, beforefind,count, anddistinctmaterialize documents, between documents ininsertanddeletebatches, and immediately before a transaction commit is handed to storage; expiry fails withMaxTimeMSExpired(code 50). A single storage step or an in-progressupdatebatch runs to completion before the next check, so a write that expires after its first documents were published reports the expiry while those documents remain durable, exactly like an interrupted MongoDB batch. The official driver transmits a transaction'smaxCommitTimeMSasmaxTimeMSoncommitTransaction; a commit that expires before its batch is handed to storage publishes nothing, records the transaction as aborted, and carries theUnknownTransactionCommitResultlabel; the driver's commit retry then seesNoSuchTransactionwithTransientTransactionErrorand restarts the whole transaction.getMorerejectsmaxTimeMSwithBadValuebecause NoSQLDB has no awaitData cursors, and a literalmaxCommitTimeMSfield is rejected withInvalidOptions. - Database waits. DDL (
create,createIndexes,drop,dropIndexes,collMod,dropDatabase,renameCollection,$out/$merge) holds its database exclusively; other commands on that database wait for it, as they wait for a MongoDB collection or database lock. A command outside a transaction waits first in, first out for as long as it may run: untilmaxTimeMS(MaxTimeMSExpired, code 50), until its client disconnects or shuts down its sending side (the wait ends withClientDisconnect, code 279, which a client that still reads receives before the connection closes) or until the server shuts down (InterruptedAtShutdown, code 91). A DDL command likewise waits for the open transactions on its database to commit or abort, and every command on that database that arrives after the DDL queues behind it, including reads of other collections. An open transaction therefore delays DDL and the commands behind it at most until the transaction lifetime limit (60 seconds, MongoDB'stransactionLifetimeLimitSeconds) aborts it. A transaction statement that needs a further database keeps a fixed five-second limit and then fails withIllegalOperation(code 20,EFENCE_BUSY: …), so a transaction never waits without bound while it holds its databases; like any failed statement, it aborts the transaction and releases its databases. The service operations (export, import, tenant and publication fencing) keep the same limit and reportEFENCE_BUSY.
Callers may therefore rely on a successful NoSQLDB response as a durable, single-node acknowledgement. They must not read it as a replica-backed acknowledgement: majority semantics across several voters arrive with replication.
Unsupported Administrative Commands
NoSQLDB returns CommandNotFound (code 59) for the wire commands validate,
legacy authenticate, hostInfo,
whatsmyuri, getLog, getCmdLineOpts, getParameter, setFreeMonitoring,
currentOp, killOp, top, profile, compact, reIndex, fsync, and
connPoolSync. These commands previously had placeholder success responses but
do not yet implement their advertised MongoDB semantics. Authentication and
authorization errors retain precedence; otherwise rejection occurs before
database publication permits, session or transaction creation, and command
dispatch. This restriction applies to the wire validate command, not the
supported offline --validate-data tool. getFreeMonitoringStatus remains
available and reports NoSQLDB's fixed disabled state.
Rust Crate Architecture 🦀
The Rust engine is organized as a Cargo workspace with 12 focused crates:
| Crate | Purpose |
|---|---|
rustdb |
Binary entry point: TCP/Unix listener, management IPC, CLI |
rustdb-config |
Server configuration types (serde, camelCase JSON) |
rustdb-wire |
Wire protocol parser/encoder (OP_MSG, OP_QUERY, OP_REPLY) |
rustdb-query |
Query matcher, update engine, aggregation, sort, projection |
rustdb-state |
Internal deterministic storage commands, stable retry identities, and durable receipts |
rustdb-kernel |
Native paged committed-log kernel with stable timelines, contiguous LSNs, strict recovery and exact receipt replay |
rustdb-replication |
Internal non-serving OpenRaft 0.9.25 one-voter fresh-root foundation; not linked into the released rustdb server |
rustdb-storage |
Native catalogs, documents, exact counts, staged imports, reclamation and bounded OpLog |
rustdb-index |
Native index records, query plans, uniqueness, bounded builds and TTL |
rustdb-txn |
Transaction + session management with snapshot isolation |
rustdb-auth |
SCRAM-SHA-256 credential handling, user metadata persistence, RBAC checks |
rustdb-commands |
40+ command handlers wiring everything together |
The native MVCC engine serves both memory and file storage.
Its Rust APIs use mvcc modules and Mvcc type names independently of npm
versions and persisted format versions. The K2 kernel stores immutable paged
roots, typed BSON records and checksummed commit markers in one append stream.
Snapshots pin roots; normal writes append changed pages. Descriptor-relative
access and the enclosing native file-root lease protect the entire lifecycle.
Ambiguous publication fences the owner until recovery. Confirmed close drains
requests, sessions, cursors and retained workers before releasing ownership.
The bounded page cache retains validated key prefixes for point lookups when large separators cannot fit as complete nodes. Prefix coverage grows with the probe length and preserves exact ordering; scans and writes use complete keys. This keeps unrelated small-key operations from repeatedly loading wide index separators without increasing the cache budget or changing the stored format.
Document, catalog, exact-count, natural-order, index-entry, unique-owner,
authentication, fencing and retry-receipt records share atomic publications.
Transactions retain changed documents and immutable read views with byte and
concurrency admission; they do not copy a complete database snapshot or impose a
10,000-document base-snapshot ceiling. Reads, getMore, indexes and streaming
aggregation use pinned bounded pages. Transaction sorts use the shared external
sort executor. Successful retryable statements retain exact results, including
generated IDs and zero-match outcomes, across restart. Failed statements do not
receive success receipts. Session termination drains active work and retires its
cursors.
Custom indexes build in bounded batches under a writer reservation and become Ready together. Startup resumes interrupted builds under native ownership before serving. If a resumed build confirms duplicate keys or exceeds its work budget, its Building state remains intact and a secret-free warning identifies the need for explicit index repair. Reads avoid that index; indexed writes fail closed. Use the supported index drop/recreate operation to resolve it. Cancellation, corruption and uncertain publication fail startup. Held databases remain closed and are skipped during recovery; releasing a hold does not run extra mutations. Their interrupted indexes can recover on a later restart.
Index names and aliases, sparse and unique constraints, empty collections, restricted-view catalogs and exact BSON survive supported database export/import. Native import replaces the entire data catalog and its publication receipt in one commit, preserving managed principal and allocation identities. Old pinned reads retain the previous catalog. Exact fenced retries append nothing; held replacements remain inaccessible until their exact receipt is released.
The serving profile is:
| Surface | Native support |
|---|---|
| Runtime | Linux and macOS; memory or current native file storage |
| Wire data | find, count, distinct, aggregate, getMore, killCursors, insert, update, delete, findAndModify; supported $out/$merge |
| Catalog/admin | Collection/database/index DDL, rename, listings, supported statistics and diagnostics, user/role administration |
| Auth/sessions | SCRAM-SHA-256, logical sessions, snapshot transactions, commit/abort and durable retryable writes |
| Management | Health, tenants, grants, attestation, fencing, publication holds, export/import and content digest |
| Debug | Bounded current-session OpLog, atomic data revert, document/collection pages and dashboard |
| Maintenance | Bounded TTL expiry, retired record reclamation, orphan import retirement and generation compaction |
| Stopped storage | Native inspection and relocation; isolated authenticated rehearsal on its qualified platform profile |
| Import/control admission | 100 MiB encoded import; bounded private staging and a small final catalog/receipt commit. Other control requests: 1 MiB |
| External cluster | Replication and HA remain unsupported; the one-voter OpenRaft foundation is not linked into serving |
These are operation and resource budgets, not total database/document limits. Unsupported options and commands reject explicitly. Imports that cannot fit one derived document or index entry within the native commit budget reject before publishing the replacement. Reclamation preserves pinned roots and retires parent identities only after their children have been handled. Automatic compaction requires at least 64 MiB of growth and 50% growth over its prior baseline.
There is one serving engine and no Bitcask fallback. No historical storage or
authentication conversion runs during startup or rehearsal. Conversion code,
when explicitly commissioned, belongs exclusively in top-level ts_migration/.
NoSQLDB makes no general MongoDB performance or feature-parity claim. Versioned packages contain Linux amd64/arm64 and macOS amd64/arm64 native executables through @git.zone/tsrust.
Multi-host release artifacts
Consumers obtain the matching engines from the same versioned
@lossless.org/nosqldb npm tarball on npmjs or Verdaccio. It includes
dist_rust/rustdb_linux_amd64, dist_rust/rustdb_linux_arm64,
dist_rust/rustdb_linux_amd64_musl, dist_rust/rustdb_linux_arm64_musl,
dist_rust/rustdb_macos_amd64 and dist_rust/rustdb_macos_arm64, each accompanied
by <binary>.tsrust-build.json. These provenance files contain binarySha256,
the package version, Git commit and target. Verify the tarball's registry
integrity and each executable's SHA-256 against that matching provenance file
before installation. The TypeScript API selects static musl executables on all Linux hosts,
including Alpine. The GNU-named Linux artifacts remain available for explicit selection
and dynamically load replaceable system glibc, requiring a GNU/Linux
host with glibc 2.34 or later, the matching loader, libm.so.6 and libgcc_s.so.1;
The musl artifacts have no dynamic loader dependency.
The native distribution terms and qualified library requirements are recorded in
third-party notices. Spark can
extract and install these files independently of its compiled JavaScript bundle;
NoSQLDB does not download engines at startup.
A plain tsrust build, as run by pnpm build and before pnpm test, compiles only the configured targets of the host's own architecture (its GNU and musl engines on Linux); tsrust --configured-targets builds the host's complete partition from targetsByHost. The development suite checks the packaged engines of the host architecture. Releases use tsrust matrix to coordinate both partitions after the version commit exists, then publish only after native verification passes and every exact-commit binary has executed and passed strict provenance assembly.
The release coordinator needs Git, Node.js, pnpm and SSH access to the configured macOS builder. The local Linux amd64 builder needs aarch64-linux-gnu-gcc, x86_64-linux-musl-gcc and aarch64-linux-musl-gcc and a registered Linux arm64 binfmt interpreter. Linux file storage requires kernel 5.6 or newer with openat2 enabled. Cross-architecture file qualification requires a full-system Linux guest or a native host. A linux-user executable probe does not qualify secure filesystem syscalls such as openat2. The built-package acceptance script test/verify-alpine.mjs exercises authenticated nested document persistence and restart through Node and Deno on Alpine; run it on both Linux architectures before publication. The Apple Silicon builder needs Rosetta. The matrix qualifies macOS first and then Linux; both partitions must pass before artifact assembly. Set these environment variables to authorized builders and their dedicated temporary roots:
export NOSQLDB_RELEASE_MACOS_SSH="release-user@mac-builder.example"
export NOSQLDB_RELEASE_MACOS_TEMP_ROOT="/absolute/dedicated/nosqldb-release"
gitzone release first runs pnpm run release:check-builders, before creating a version commit or tag. It verifies the project-local tsrust version, compiles and executes the required architecture probes, and rejects missing cross-toolchains or emulation, an unsuitable macOS host, missing tools, or an unsafe remote temporary root. The release build then:
- Builds the TypeScript package and debug UI bundle from the version commit.
- Transfers an exact Git bundle into isolated local Linux and SSH macOS worker workspaces and installs each frozen lockfile.
- Runs the configured
release:verify-nativecommand on each worker: the full Rust workspace, TypeScript test checks, atsrust --configured-targetsbuild of the worker's partition, the public API suite with the packaged-engine checks for both architectures, and explicit-engine authentication/ownership/rehearsal tests through the worker's other packaged architecture. Tests use short, private disposable filesystem roots under/var/tmpon Linux and canonical/tmpon macOS. Linux builders must provide/var/tmpon a native-supported durable filesystem; tmpfs is rejected. Failures retain diagnostics and prevent assembly. - Builds and executes GNU and musl Linux amd64/arm64 plus macOS arm64/amd64 binaries on their assigned workers.
- Retrieves bounded artifact sets and transactionally publishes the complete provenance-checked matrix into
dist_rust/.
The qualified musl toolchains are cross-tools/musl-cross 20260823, GCC 16.2.0 with musl 1.2.6. Expose the upstream toolchain bin directories on PATH and alias their full *-unknown-linux-musl-gcc names to the compiler names above. Rust supplies its pinned, patched musl runtime; its notices and the cross-toolchain runtime notices are included with the binaries.
No host address or path is stored in the repository. A failed run retains its unique local diagnostics and owner-marked remote diagnostics, then prints their paths. Remote cleanup is an owner-validated publication gate, and an incomplete matrix never replaces the prior dist_rust/.
Storage Engine Reliability 🔒
- Atomic native publications include data, indexes, authentication, fences and durable outcomes. File writes are acknowledged after durable commit.
- Immutable roots keep reads stable through concurrent writes and compaction.
- Checksums, frame identities and contiguous positions validate committed history. Only a valid incomplete final append is repairable during owned startup; complete or interior corruption is rejected.
- Uncertain publication fences the engine until confirmed recovery.
- Bounded maintenance reclaims unreachable records and obsolete generations after their readers release them.
- Unix listener startup preserves active sockets and non-socket targets. Verified stale sockets are recoverable; cleanup checks the owned inode.
Data Integrity CLI 🔍
./dist_rust/rustdb_linux_amd64 --validate-data /path/to/data
# {"schemaVersion":1,"databases":[{"name":"mydb","collections":["users"]}]}
The CLI uses the same stopped native namespace inspector. It validates storage, catalogs and indexes while retaining the existing native leases, without starting a listener or repairing the source. It returns JSON on success and a nonzero exit on failure. No configuration file is required. The operation has a one-hour deadline, 100,000 filesystem-entry and 1 TiB byte limits, and bounded native record/result admission. This is storage integrity evidence; it does not replace the full authenticated rehearsal or production ownership protocol.
Testing Example
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { NoSqlDbServer } from '@lossless.org/nosqldb';
import { MongoClient } from 'mongodb';
let server: NoSqlDbServer;
let client: MongoClient;
tap.test('setup', async () => {
server = new NoSqlDbServer({ port: 27117 });
await server.start();
client = new MongoClient('mongodb://127.0.0.1:27117', { directConnection: true });
await client.connect();
});
tap.test('should insert and find', async () => {
const col = client.db('test').collection('items');
await col.insertOne({ name: 'Widget', price: 9.99 });
const item = await col.findOne({ name: 'Widget' });
expect(item?.price).toEqual(9.99);
});
tap.test('should track changes in oplog', async () => {
const oplog = await server.getOpLog();
expect(oplog.entries.length).toBeGreaterThan(0);
expect(oplog.entries[0].op).toEqual('insert');
});
tap.test('teardown', async () => {
await client.close();
await server.stop();
});
export default tap.start();
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.