@lossless.org/nosqldb 10.11.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.smartconfig.json +117 -0
  2. package/dist_rust/rustdb_linux_amd64 +0 -0
  3. package/dist_rust/rustdb_linux_amd64.tsrust-build.json +5 -5
  4. package/dist_rust/rustdb_linux_amd64_musl +0 -0
  5. package/dist_rust/rustdb_linux_amd64_musl.tsrust-build.json +5 -5
  6. package/dist_rust/rustdb_linux_arm64 +0 -0
  7. package/dist_rust/rustdb_linux_arm64.tsrust-build.json +5 -5
  8. package/dist_rust/rustdb_linux_arm64_musl +0 -0
  9. package/dist_rust/rustdb_linux_arm64_musl.tsrust-build.json +5 -5
  10. package/dist_rust/rustdb_macos_amd64 +0 -0
  11. package/dist_rust/rustdb_macos_amd64.tsrust-build.json +5 -5
  12. package/dist_rust/rustdb_macos_arm64 +0 -0
  13. package/dist_rust/rustdb_macos_arm64.tsrust-build.json +5 -5
  14. package/dist_ts/00_commitinfo_data.js +2 -2
  15. package/dist_ts/ts_local/classes.localnosqldb.d.ts +15 -4
  16. package/dist_ts/ts_local/classes.localnosqldb.js +31 -11
  17. package/dist_ts/ts_nosqldb/index.d.ts +3 -3
  18. package/dist_ts/ts_nosqldb/index.js +5 -4
  19. package/dist_ts/ts_nosqldb/rust-db-bridge.d.ts +125 -2
  20. package/dist_ts/ts_nosqldb/rust-db-bridge.js +261 -46
  21. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.d.ts +69 -3
  22. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.js +91 -12
  23. package/dist_ts/ts_nosqldb/server/index.d.ts +2 -2
  24. package/dist_ts/ts_nosqldb/server/index.js +2 -2
  25. package/dist_ts/ts_nosqldb/stopped-storage.d.ts +9 -1
  26. package/dist_ts/ts_nosqldb/stopped-storage.js +1 -1
  27. package/dist_ts/ts_nosqldb/storage-rehearsal.js +1 -1
  28. package/dist_ts/ts_nosqldb/storage-root-alignment.js +7 -3
  29. package/dist_ts/ts_nosqldb/storage-root-relocation.js +2 -2
  30. package/dist_ts_debugserver/bundled.js +6 -2
  31. package/package.json +28 -15
  32. package/readme.md +470 -730
  33. package/third-party-notices.md +40 -8
  34. package/third_party_licenses/inventory.json +6 -6
  35. package/ts/00_commitinfo_data.ts +8 -0
  36. package/ts/index.ts +85 -0
  37. package/ts/ts_local/classes.localnosqldb.ts +631 -0
  38. package/ts/ts_local/index.ts +19 -0
  39. package/ts/ts_local/plugins.ts +1 -0
  40. package/ts/ts_nosqldb/classes.storagerehearsal.ts +109 -0
  41. package/ts/ts_nosqldb/committed-writes.ts +38 -0
  42. package/ts/ts_nosqldb/file-storage-options.ts +33 -0
  43. package/ts/ts_nosqldb/file-storage-startup.ts +10 -0
  44. package/ts/ts_nosqldb/index.ts +132 -0
  45. package/ts/ts_nosqldb/offline-inspection.ts +375 -0
  46. package/ts/ts_nosqldb/plugins.ts +12 -0
  47. package/ts/ts_nosqldb/resource-fencing.ts +368 -0
  48. package/ts/ts_nosqldb/rust-db-bridge.ts +1309 -0
  49. package/ts/ts_nosqldb/server/classes.nosqldbserver.ts +874 -0
  50. package/ts/ts_nosqldb/server/index.ts +4 -0
  51. package/ts/ts_nosqldb/service-types.ts +499 -0
  52. package/ts/ts_nosqldb/stopped-storage.ts +116 -0
  53. package/ts/ts_nosqldb/storage-rehearsal.ts +215 -0
  54. package/ts/ts_nosqldb/storage-root-alignment.ts +180 -0
  55. package/ts/ts_nosqldb/storage-root-relocation.ts +279 -0
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lossless.org/nosqldb
2
2
 
3
- 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.
3
+ A MongoDB-wire-compatible database server powered by its own native Rust MVCC engine, engineered from scratch in a clean room with a public API surface derived from the Apache-2.0-licensed official MongoDB Node.js driver, and built for every scale: from a test fixture or a database embedded in a single application to large enterprise deployments. Its goal is to beat MongoDB on performance and on features. It supports the documented command surface through the official `mongodb` driver without an external MongoDB server. Memory and file storage use the same format-3 store, including the operation log, point-in-time revert and debug dashboard. Matching executables are bundled with the package.
4
4
 
5
5
  ## Issue Reporting and Security
6
6
 
@@ -12,32 +12,29 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
12
12
  pnpm add @lossless.org/nosqldb
13
13
  ```
14
14
 
15
- ## Moving from @push.rocks/smartdb
15
+ ---
16
16
 
17
- The module has moved to `@lossless.org/nosqldb`. Install the new package and update imports and product-specific names together:
17
+ ## What It Does
18
18
 
19
- | Previous name | New name |
20
- | --- | --- |
21
- | `@push.rocks/smartdb` | `@lossless.org/nosqldb` |
22
- | `SmartdbServer` | `NoSqlDbServer` |
23
- | `LocalSmartDb` | `LocalNoSqlDb` |
24
- | `SmartdbStorageRehearsal` | `NoSqlDbStorageRehearsal` |
25
- | `SmartdbDebugServer` / `SmartdbDebugUi` | `NoSqlDbDebugServer` / `NoSqlDbDebugUi` |
26
- | `ISmartdb*`, `ISmartDb*`, `TSmartdb*`, `TSmartDb*` | `INoSqlDb*`, `TNoSqlDb*` |
27
- | `createSmartDb*`, `normalizeSmartDb*`, `smartDb*` | `createNoSqlDb*`, `normalizeNoSqlDb*`, `noSqlDb*` |
28
- | `SMARTDB_*` environment variables | `NOSQLDB_*` environment variables |
29
- | `<smartdb-debugui>` / `/api/smartdb/*` | `<nosqldb-debugui>` / `/api/nosqldb/*` |
30
- | Benchmark `--engine smartdb` | Benchmark `--engine nosqldb` |
19
+ `@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.
31
20
 
32
- 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.
21
+ ### From one process to enterprise scale
33
22
 
34
- 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.
23
+ NoSQLDB brings its own clean-room engineered, MongoDB-compatible engine to every stage of an application's life:
35
24
 
36
- ---
25
+ - **Development and tests:** start a server from code, in memory or on disk, with nothing else to install.
26
+ - **Embedded:** ship the database inside your application and reach it over TCP or a Unix socket.
27
+ - **Enterprise scale:** large datasets, many collections and indexes, and many concurrent clients on its own durable MVCC engine.
37
28
 
38
- ## What It Does
29
+ The target is to beat MongoDB on performance and on features: more throughput, lower latency, and less memory and disk per operation, with MongoDB-compatible behaviour wherever applications rely on it, and capabilities beyond it where they help, such as the built-in operation log, point-in-time revert and debug dashboard.
39
30
 
40
- `@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.
31
+ ### MongoDB compatibility
32
+
33
+ NoSQLDB is an independent implementation, written from scratch to be MongoDB compatible. Its public API surface is derived from the client side: the official MongoDB Node.js driver (`mongodb`, Apache-2.0 licensed) and the commands and replies it exchanges. Compatibility is established by runtime testing against MongoDB through its wire protocol and the official drivers. NoSQLDB contains no MongoDB server code. It is built with the clean-room setup of [AGL](https://code.foss.global/modelprofile.com/agl): the sessions that write its code work in confined containers that hold nothing but this repository. Where this readme says a behaviour, code or message is MongoDB compatible, it was implemented to match what such testing observes.
34
+
35
+ Contributions follow the same clean-room rules: describe behaviour as implemented to be MongoDB compatible, never as the same as a MongoDB version; record provenance as observed by runtime testing against MongoDB; and never read, quote, cite or link MongoDB server source code or its test suites.
36
+
37
+ MongoDB is a trademark of MongoDB, Inc. NoSQLDB is not affiliated with or endorsed by MongoDB, Inc.
41
38
 
42
39
  ### Why NoSQLDB?
43
40
 
@@ -49,7 +46,7 @@ This is a package and API rename, not a storage-format conversion. Existing curr
49
46
  | **Persistence** | Memory or file-based | Full disk engine |
50
47
  | **Debug UI** | Built in | External tooling |
51
48
  | **Point-in-time revert** | Built in | Depends on the database |
52
- | **Perfect for** | Unit tests, CI/CD, prototyping, local dev, embedded | Production at scale |
49
+ | **Built for** | Unit tests, CI/CD, local dev, embedded applications and large-scale production | Production |
53
50
 
54
51
  ### Three Ways to Use It
55
52
 
@@ -252,7 +249,7 @@ Each entry contains:
252
249
 
253
250
  ## 🔢 Document Field Order
254
251
 
255
- Like MongoDB, every stored document leads with `_id`. Inserts, upserts, replacements, `findAndModify`, `$out` and `$merge` move a client-supplied `_id` to the front (the Node.js driver appends its generated `_id` after the caller's fields) and generate a missing one there. Every other field keeps its written order: update operators change existing fields in place and append the fields they create, and a replacement keeps its own field order after `_id`. Documents stored with a trailing `_id` by 10.7.3 and earlier keep it until an update, replacement or `$merge` changes them.
252
+ Every stored document leads with `_id`, implemented to be MongoDB compatible. Inserts, upserts, replacements, `findAndModify`, `$out` and `$merge` move a client-supplied `_id` to the front (the Node.js driver appends its generated `_id` after the caller's fields) and generate a missing one there. Every other field keeps its written order: update operators change existing fields in place and append the fields they create, and a replacement keeps its own field order after `_id`. Documents stored with a trailing `_id` by 10.7.3 and earlier keep it until an update, replacement or `$merge` changes them.
256
253
 
257
254
  ---
258
255
 
@@ -260,7 +257,7 @@ Like MongoDB, every stored document leads with `_id`. Inserts, upserts, replacem
260
257
 
261
258
  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`.
262
259
 
263
- As in MongoDB, a rewrite is skipped only when the document stays identical: the same fields in the same order with the same values and BSON types, at every depth. Only the position of `_id` is ignored, so a document stored with a trailing `_id` by 10.7.3 and earlier is not rewritten by a write that changes nothing. A replacement or `$set` that only reorders fields is written. Explicit changes to `_updatedAt` are persisted and returned just like changes to any other field. Applications decide when to advance their timestamps.
260
+ To be MongoDB compatible, a rewrite is skipped only when the document stays identical: the same fields in the same order with the same values and BSON types, at every depth. Only the position of `_id` is ignored, so a document stored with a trailing `_id` by 10.7.3 and earlier is not rewritten by a write that changes nothing. A replacement or `$set` that only reorders fields is written. Explicit changes to `_updatedAt` are persisted and returned just like changes to any other field. Applications decide when to advance their timestamps.
264
261
 
265
262
  Counters are exposed through `serverStatus`:
266
263
 
@@ -338,6 +335,59 @@ try {
338
335
 
339
336
  The socket parent directory must already exist with the caller's desired access
340
337
  permissions. `RustDbBridge` also accepts `{ binaryPath }` for direct lifecycle use.
338
+ When its `spawn()` returns false, `spawnFailureMessage` says what that means as
339
+ far as the bridge knows: with a `binaryPath`, that the engine there did not
340
+ report ready within the bridge's limit, with the path as given and the time
341
+ waited; without one, that no engine binary was found or that the engine found
342
+ did not report ready, as nothing tells which. If ending that engine fails,
343
+ `spawn()` throws instead of returning false, and `spawnFailureMessage` keeps the
344
+ message. After any other `spawn()`, including one that throws on its own
345
+ `timeoutMs` or `signal`, it is undefined. A `binaryPath` that does not exist
346
+ is refused before `spawn()` returns, with `RustBinaryLocatorError`.
347
+
348
+ #### Engine output
349
+
350
+ The engine writes its log to its standard error. None of it reaches the host
351
+ process's standard error: by default the server keeps only the engine's last
352
+ lines, for errors. To receive the log, pass `engineLog`, which is called once
353
+ for every line, in order, without the line break. `onEngineExit` receives a
354
+ `NoSqlDbEngineExitError` for each unexpected exit of the engine: an exit with a
355
+ non-zero status or by a signal while the server runs and no stop is in
356
+ progress. An exit with status 0, which the engine makes after a graceful
357
+ shutdown it was signalled to make, is not reported; the server counts as
358
+ stopped either way.
359
+
360
+ ```typescript
361
+ import { NoSqlDbServer } from '@lossless.org/nosqldb';
362
+
363
+ const server = new NoSqlDbServer({
364
+ storage: 'file',
365
+ storagePath: '/var/lib/app/nosqldb',
366
+ engineLog: (line) => logger.info(`nosqldb: ${line}`),
367
+ onEngineExit: (error) => logger.error(error.message, error.engineStderrTail),
368
+ });
369
+ ```
370
+
371
+ Whatever is set, these carry the engine's last 64 standard-error lines in their
372
+ field `engineStderrTail`, a frozen array of strings, without any change to
373
+ their message:
374
+
375
+ - every rejection of `start()` of `NoSqlDbServer` and `LocalNoSqlDb`, with an
376
+ empty array when the start failed before it spawned an engine;
377
+ - the `NoSqlDbEngineExitError`, whose message is
378
+ `nosqldb Rust process exited unexpectedly (code=<code>, signal=<signal>)` and
379
+ which has `exitCode` and `signal`;
380
+ - the rejection of a management call that fails because the engine no longer
381
+ runs.
382
+
383
+ A line longer than 1,024 characters keeps its first 1,024, followed by
384
+ ` [<n> more characters]`; `engineLog` receives it whole.
385
+ `noSqlDbEngineStderrTailLimits` exports both bounds. `LocalNoSqlDb` accepts
386
+ both options and passes them to its server. `RustDbBridge` accepts `engineLog`
387
+ as well, and its `engineStderrTail` getter returns the last lines of the
388
+ running or most recently spawned engine. Each function runs in its own
389
+ microtask, after the server has recorded the line or the exit; an exception it
390
+ throws is not caught and reaches the process as an uncaught exception.
341
391
 
342
392
  #### Native memory and file storage
343
393
 
@@ -380,54 +430,68 @@ above is an example, not a universal default. Reopen with the stored SCRAM work
380
430
  factor; bootstrap users may be omitted after initialization. `LocalNoSqlDb`
381
431
  accepts `folderPath`, `socketPath`, `binaryPath` and the same `auth` options.
382
432
 
433
+ The engine allocates its memory with jemalloc, under settings built into the
434
+ engine: two arenas, freed memory returned to the operating system over 5
435
+ seconds (dirty decay 5,000 ms, muzzy decay 0), and on Linux the allocator's
436
+ background threads. At start the engine checks that its allocator runs these
437
+ settings and raises its soft open-files limit to its hard limit (on macOS to at
438
+ most `kern.maxfilesperproc`). When either fails, as the check does when
439
+ `_RJEM_MALLOC_CONF` in its environment changes a setting, the engine does not
440
+ start: it exits with status 71, and `start()` rejects with the reason in
441
+ `engineStderrTail`. The `MIMALLOC_ARENA_EAGER_COMMIT` and
442
+ `MIMALLOC_ALLOW_LARGE_OS_PAGES` variables that earlier releases set for the
443
+ engine are no longer set or read.
444
+
445
+ The performance figures this release states are measured with file storage on
446
+ XFS. ext4 was measured only on a loop-backed substitute, so this release makes
447
+ no latency claim for ext4. Recovery after a crash relies on a filesystem that
448
+ never exposes stale blocks after a crash: XFS, or ext4 with ordered data
449
+ (`data=ordered`, its default mode). ext4 mounted with `data=writeback` is not
450
+ supported. On macOS, file storage runs on APFS; the durability gates and the
451
+ timed gates of this release run on Linux only.
452
+
383
453
  #### Native query and maintenance efficiency
384
454
 
385
455
  Bounded synchronous reads, including principal resolution, publication authority
386
- and query setup, share a retained generation lease. No lease or resolved
387
- authorization is reused across requests or asynchronous suspensions.
456
+ and query setup, share one retained snapshot of the root. No such snapshot or
457
+ resolved authorization is reused across requests or asynchronous suspensions.
388
458
 
389
459
  Filesystem ownership and root bindings are validated once per command for
390
460
  reads: the reads of each wire command or background maintenance turn reuse one
391
- full validation of the root and its bindings that began after the command
392
- started, run by its own first storage read or by a concurrent command, and
393
- every retained generation file's binding is checked once per command. Reads go
394
- through retained descriptors, so a path swap cannot change what they return. A
395
- failed validation revokes every earlier one, so commands still running stop at
396
- their next read. A validation covers reads for at most one second after it
397
- began, measured on a monotonic clock, so a long-running command such as a large
398
- aggregation or sort validates again at its first read after that and fails at
399
- that read once its root or a binding has been replaced. Every mutation,
400
- including commit conflict checks, appends, flushes, compaction, relocation and
401
- directory changes, still validates ownership and bindings before and after
402
- itself, and startup, recovery and native management calls, which run outside
403
- wire commands, validate every access. A root, container or generation file
461
+ full validation of the enclosing owner, the root and its bindings that began
462
+ after the command started, run by its own first storage read or by a
463
+ concurrent command. Reads go through retained descriptors, so a path swap
464
+ cannot change what they return. A failed validation revokes every earlier
465
+ one, so commands still running stop at their next read. A validation covers
466
+ reads for at most one second after it began, measured on a monotonic clock,
467
+ so a long-running command such as a large aggregation or sort validates again
468
+ at its first read after that and fails at that read once its root or a
469
+ binding has been replaced. Every write, whether a commit's, a group's, a
470
+ maintenance operation's or an index build's, validates the enclosing owner
471
+ and every binding before it writes, and a commit's conflict checks validate
472
+ in full before and after themselves, before the commit takes its place in
473
+ the log. After a write, when its outcome can be unknown, the root's bindings
474
+ and each file the write wrote, by its binding and its length, are checked
475
+ through the retained descriptors alone, without opening the owner's path; a
476
+ failure there fences the root, and the next command's first access validates
477
+ the enclosing owner again. Startup, recovery and native management calls,
478
+ which run outside wire commands, validate every access. A root or container
404
479
  renamed or replaced between commands fails the next command's first storage
405
- access. Durability, cancellation and close checks remain enforced.
406
-
407
- Compound indexes combine leading equality fields, if any, with a range on the
408
- next field. For example, `{ serverId: 1, timestamp: 1 }` seeks directly into the
409
- requested server's time interval. Inclusive/exclusive endpoints, descending
410
- components, trailing fields and reverse pagination retain their normal
411
- semantics. A range is built from `$gt`, `$gte`, `$lt` and `$lte`, and from
412
- `$ne: null` or a `$nin` list holding null, which read only the non-null keys as
413
- MongoDB's `[MinKey, undefined) ∪ (null, MaxKey]` bounds do: documents without
414
- the field or with an explicit null are not examined. Ranges are built only on
415
- complete indexes whose values all have a scalar order, and on a list field from
416
- one side of the range only (see **List fields** below); other indexes are not
417
- narrowed. The full predicate is still checked for every selected candidate.
480
+ access, and a data or log file replaced underneath the root fails the next
481
+ write that writes it. Durability, cancellation and close checks remain
482
+ enforced.
418
483
 
419
484
  For a query whose filter references only `_id` and whose projection is exactly
420
485
  `{ _id: 1 }`, the native primary index can supply the original BSON identity
421
486
  without loading the document payload. Range endpoints, order, skip, limit and
422
487
  cursor pagination retain their normal semantics. The engine verifies the index
423
- and unique-owner values, the document frame's existence, and either matching
424
- commit versions or a storage-owned SHA-256 certificate of the exact BSON `_id`.
425
- Updates that keep the same identity leave the primary owner and its certificate
488
+ and unique-owner values, the document record's existence, and the storage-owned
489
+ SHA-256 certificate of the exact BSON `_id` that every document record holds.
490
+ Updates that keep the same identity leave the primary owner and the certificate
426
491
  unchanged, avoiding an extra index rewrite. Numeric BSON width changes update
427
492
  the certificate and primary identity atomically with the body. A covered read
428
- does not checksum an unrequested document payload. Older records without a
429
- usable identity proof use the normal bounded body read; the next actual write
430
- adds the certificate.
493
+ does not checksum an unrequested document payload. An entry whose owner names
494
+ another identity than the certificate uses the normal bounded body read.
431
495
  Secondary-index projections and predicates requiring other fields retain normal
432
496
  document reads. No application cache or configuration flag is required.
433
497
 
@@ -449,27 +513,20 @@ discard their uncommitted changes. An admitted command that reaches the closing
449
513
  session owner fails with `ShutdownInProgress` (code 91), and one still waiting
450
514
  for a database or collection gate fails with `InterruptedAtShutdown` (code
451
515
  11600); a transaction statement carries `TransientTransactionError` with
452
- either, as in MongoDB 8. Successful shutdown releases storage-root
516
+ either, implemented to be MongoDB compatible. Successful shutdown releases storage-root
453
517
  ownership and exits with code 0 even when the parent's management pipe remains
454
518
  open. A second shutdown signal, a drain failure, or the overall 40-second
455
519
  deadline forces exit with code 70; forced exit does not promise drain completion.
456
520
  `NoSqlDbServer.stop()` also succeeds after a confirmed engine exit without
457
521
  sending another management request to the dead child.
458
522
 
459
- **Storage compatibility:** version 10 adds a 40-byte certified document-ordinal
460
- record alongside the legacy 8-byte ordinal. It reads existing stores directly;
461
- startup and reads perform no format rewrite. After version 10 writes certified
462
- records, use version 10 or later to reopen that store. Older binaries reject the
463
- new record representation. Keep a pre-upgrade backup if a binary downgrade is
464
- required. The K2 pages, commit markers and recovery protocol are unchanged.
465
-
466
523
  Maintenance batches catalog work and parks completed reclamation and no-TTL
467
524
  sweeps until the committed position changes. Session cleanup skips its metadata
468
- scan until a session can expire. Compaction uses the persisted generation anchor
469
- as its baseline, so reopening an already-compacted root does not rewrite it.
470
- Compaction runs separately from TTL maintenance, observes shutdown cancellation,
471
- and does not restart a large copy because a short maintenance deadline elapsed.
472
- These maintenance optimizations preserve the existing generation and commit formats.
525
+ scan until a session can expire. The store maintains itself on its own thread:
526
+ it merges its write buffer into the tree, writes checkpoints, prunes the
527
+ tombstones no snapshot needs and moves the live data out of data segments
528
+ that hold much garbage. No pass copies the whole root, and there is no
529
+ compaction to run, resume or restart.
473
530
 
474
531
  #### File startup admission
475
532
 
@@ -480,20 +537,26 @@ The exported type is `TNoSqlDbFileStorageStartup`. Invalid values, including
480
537
  `'migrate'`, reject at construction. Selecting the policy with memory storage
481
538
  also rejects.
482
539
 
483
- Default startup accepts a fresh root or a current native root. Use
484
- `fileStorageStartup: 'existing-only'` when the database must already exist:
485
- missing paths, empty directories and identity-only roots without committed
486
- initialization fail instead of creating an empty database. The native owner
487
- enforces this policy; no TypeScript existence check or fallback can create a
488
- new root. Older binaries reject the new option rather than silently ignoring it.
540
+ Default startup accepts a fresh root or a current native root, and converts a
541
+ root that a 10.x release wrote before it serves it (see **Storage format
542
+ migration** below). Use `fileStorageStartup: 'existing-only'` when the database
543
+ must already exist: missing paths, empty directories and identity-only roots
544
+ without committed initialization fail instead of creating an empty database,
545
+ and a 10.x root that holds a committed database counts as existing and is
546
+ converted as under the default. The native owner enforces this policy; no
547
+ TypeScript existence check or fallback can create a new root. Older binaries
548
+ reject the new option rather than silently ignoring it.
489
549
  Rehearsal startup retains its separate current-format-only configuration.
490
550
 
491
- Foreign, old, mixed and
492
- orphaned migration staging layouts are rejected without conversion. TypeScript
551
+ Foreign and mixed layouts, a 10.x name beside entries of this release without
552
+ the record of a conversion in progress, and a 10.x root that the conversion
553
+ cannot take are rejected before anything is written. TypeScript
493
554
  performs no storage writes before starting the engine. Native format admission
494
555
  uses the owning detector; the exclusive native file-root lease is acquired before
495
556
  initialization, recovery, authentication or index preparation.
496
557
 
558
+ A root that carries a format or a feature that this release does not know, such as a root that a later release wrote, is refused before anything is written, and no root is created in its place or beside it. The start's rejection then carries `responseErrorCode: 'UNKNOWN_ROOT'`, which no other start failure carries, and its message names the cause. A missing storage path is created together with every directory missing above it, each accessible to its owner alone; `'existing-only'` creates none of them.
559
+
497
560
  Only after `await server.start()` succeeds should consumers prepare SmartData
498
561
  models or run application migrations. Start cancellation and deadlines cover the
499
562
  native operation; failed-start cleanup confirms child exit, and `stop()` remains
@@ -501,10 +564,9 @@ retryable if termination cannot yet be confirmed. A second owner of the same
501
564
  root is rejected without disturbing the winner. Callers must also exclude
502
565
  manual edits and lifecycle changes outside NoSQLDB's ownership protocol.
503
566
 
504
- No automatic format or auth-permission migration runs during startup.
505
- Persisted-format conversions belong exclusively in the top-level
506
- `ts_migration/` folder and require their own explicit qualification. This
507
- release contains no old-engine or authentication-schema conversion.
567
+ **Storage format migration.** Every root this release creates is a format-3 root under `.nosqldb-control` and `.nosqldb-store`. A start, under either `fileStorageStartup` policy, converts a root that a 10.x release wrote, which it recognizes by `.__rustdb_internal` and `.__rustdb_mvcc`, and then serves the result. The conversion reads the 10.x root without writing into it, builds the format-3 root beside it under this release's names, verifies it record for record against the 10.x content, and only then removes the 10.x entries. It records its progress in `.nosqldb-conversion`, so a start that is stopped or interrupted during the conversion resumes it at the next start. Before it writes anything it declares the space it needs beside the 10.x root, and the start is refused when the file system does not hold it. It carries the database as of the root's last completed commit, with no history of writes committed before the conversion and no part of an import that had not finished. No earlier release reads a root that this release has created, converted or aligned: keep a stopped copy of the 10.x root if you may need to go back. That copy is a copied root: restored into another directory or onto another machine, it keeps the binding of its old place, and 10.8.1, like this release, refuses to start on it until it is aligned. Alignment refuses a root that holds auth users, so for such a root a copy restored that way is no way back. A start refuses, and leaves the path as it found it, a 10.x root that holds no committed database or only part of one, a 10.x name beside entries of this release without the conversion's record, a 10.x root that a running 10.x process holds or that a rehearsal created, a collection whose recorded count its documents do not hold, and a unique index that would hold two documents under one key once each array member is indexed on its own. A 10.x root that was copied, moved or restored converts too but keeps the binding of its old place; like every copied root, it must be aligned (`alignStoppedStorageRoot`) before a start serves it. No authentication or permission migration runs during startup, and this release contains no authentication-schema conversion.
568
+
569
+ A stopped root that a 10.x release wrote can be read before a start converts it. The stopped operations that only read open it read-only, under its own names and its own lock as a 10.x release's stopped operations did, and change nothing in it: the root audit (`inspectStoppedStorageRoot`), the string and namespace inspections (`inspectOfflineStringValue`, `inspectOfflinePhysicalNamespaces`) and data validation (`--validate-data`), which runs the namespace inspection. Alignment, relocation and rehearsal refuse a 10.x root with `UNSUPPORTED`: start a server on it to convert it, then copy and align the converted root. Every stopped operation refuses a path on which a conversion is in progress, one that holds `.nosqldb-conversion`; the root audit answers it as `unavailable` with `UNSUPPORTED`, and the string and namespace inspections and data validation reject it with `UNSUPPORTED`. A start on that path resumes the conversion and finishes it.
508
570
 
509
571
  ```typescript
510
572
  import { NoSqlDbServer } from '@lossless.org/nosqldb';
@@ -546,6 +608,15 @@ const lean = new NoSqlDbServer({
546
608
  oplog: { enabled: false },
547
609
  });
548
610
 
611
+ // The store's memory budget, which its node cache and write buffer share:
612
+ // 4 MiB to 1 TiB, refused outside that range and never clamped
613
+ // (default: computed from the memory the process may use)
614
+ const budgeted = new NoSqlDbServer({
615
+ storage: 'file',
616
+ storagePath: './data',
617
+ storageBudgetBytes: 256 * 1024 * 1024,
618
+ });
619
+
549
620
  // TLS transport for TCP mode
550
621
  const tlsServer = new NoSqlDbServer({
551
622
  port: 27017,
@@ -608,7 +679,7 @@ the transaction's first statement. Supported reads and writes see the
608
679
  transaction snapshot plus its staged changes. Commit publishes atomically;
609
680
  abort discards staged changes.
610
681
 
611
- Write conflicts follow MongoDB: the first statement to write a document or a
682
+ Write conflicts are implemented to be MongoDB compatible: the first statement to write a document or a
612
683
  unique index key wins, and the later writer is told at its own statement.
613
684
 
614
685
  - A transaction statement that inserts, updates, replaces, deletes or upserts a
@@ -620,8 +691,16 @@ unique index key wins, and the later writer is told at its own statement.
620
691
  `commitTransaction` report `NoSuchTransaction` (251), also labelled
621
692
  `TransientTransactionError`. The transaction that wrote first keeps its
622
693
  documents and commits. The driver's `withTransaction` retries the loser on a
623
- fresh snapshot, so a transaction that writes a hot document and then does more
624
- work makes progress against clients looping short transactions on it.
694
+ fresh snapshot, and each attempt can lose again: no priority, ageing or wait
695
+ rule favours a retrying transaction. So a transaction that writes a hot
696
+ document and then does more work makes progress against clients looping
697
+ short transactions on it only by its odds of winning each attempt, with no
698
+ bound on its attempts or its time. Those per-attempt odds are lower than
699
+ MongoDB's, as observed by runtime testing against MongoDB (`mongo:7.0`,
700
+ 7.0.39): MongoDB releases a committing transaction's documents before it
701
+ acknowledges the commit, while NoSQLDB acknowledges a commit as it releases
702
+ them, after the commit is durable. On a host whose durable sync is slower,
703
+ the odds fall further.
625
704
  - Reads never conflict: they see the snapshot, not another transaction's
626
705
  uncommitted writes. A statement that writes nothing (a filter that matches
627
706
  nothing in the snapshot, an update that leaves the document unchanged) does
@@ -635,96 +714,18 @@ unique index key wins, and the later writer is told at its own statement.
635
714
  finish. Writes to other documents are not delayed. A multi-document write
636
715
  outside a transaction stays all-or-nothing: it publishes nothing until it can
637
716
  publish every document.
638
- - A transaction open for longer than 60 seconds is aborted, as by MongoDB's
639
- transaction reaper: its staged writes are discarded and its cursors, write
640
- claims, transaction capacity and database and collection gates are
641
- released. The server
642
- checks idle sessions once a second; a statement still executing when the
643
- limit passes is not interrupted, and the transaction aborts when that
644
- statement has finished (MongoDB instead interrupts that statement with
645
- `TransactionExceededLifetimeLimitSeconds` (290)). Its later statements, `commitTransaction` and
646
- `abortTransaction` report `NoSuchTransaction` (251); statements and
647
- `commitTransaction` carry `TransientTransactionError`, so `withTransaction`
648
- retries from the start. Before that abort, the next writer that needs one of
649
- its documents aborts it too rather than wait. The limit is fixed at
650
- MongoDB's default `transactionLifetimeLimitSeconds` and is not configurable.
651
- Ending or expiring the session, and stopping the server, release a
652
- transaction's writes as well.
653
717
  - TTL expiry is not a contending writer. It leaves out the documents an open
654
718
  transaction has written, deletes the other expired documents of the index
655
719
  and moves past the held ones. A later pass over the index expires those
656
720
  still expired once the transaction has ended. It never waits for a transaction and never aborts
657
- one, whatever its age. MongoDB's TTL monitor instead retries a conflicting delete until
658
- the transaction ends and leaves aborting it to its transaction reaper.
721
+ one, whatever its age. MongoDB's TTL expiry, as observed by runtime testing, instead retries a conflicting delete until
722
+ the transaction ends and leaves aborting it to the transaction lifetime limit.
659
723
  - A key already visible in the transaction's snapshot is not a conflict:
660
724
  inserting it fails the statement with `DuplicateKey` (11000), as outside a
661
725
  transaction. A collection created inside a transaction has no stored identity
662
726
  until commit, so two transactions creating the same collection both succeed
663
- their statements and the second commit reports `WriteConflict` (112), as on
664
- MongoDB 7.
665
-
666
- #### Read fences
667
-
668
- `readFence` is a NoSQLDB command, not a MongoDB one, available from 10.11.0. It
669
- lets a transaction depend on documents it read without writing them: its commit
670
- fails if any of them changed after the transaction's snapshot. Two transactions
671
- that only read and fence one document therefore both commit, where fencing by
672
- writing the document would make one of them conflict.
673
-
674
- ```typescript
675
- await session.withTransaction(async () => {
676
- const lane = await lanes.findOne({ _id: 'dns' }, { session });
677
- await db.command({ readFence: 'lanes', ids: ['dns'] }, { session });
678
- await jobs.insertOne({ _id: 'next', basedOn: lane?.revision }, { session });
679
- });
680
- ```
681
-
682
- - `{ readFence: <collection>, ids: [...] }` answers `{ ok: 1 }`, only as a
683
- statement of a multi-document transaction; outside one it is refused with
684
- `InvalidOptions` (72). `ids` holds 1 to 1,000 `_id` values, otherwise the
685
- statement fails with `BadValue` (2). Duplicates count once, compared as
686
- `_id` values are, so `1`, `NumberLong(1)` and `1.0` are one id. It needs the
687
- read privilege of the database, as `find` does, and refuses a view with
688
- `CommandNotSupportedOnView` (166). A refused statement ends its transaction,
689
- as every failed transaction statement does.
690
- - `commitTransaction` is refused with `WriteConflict` (112), labelled
691
- `TransientTransactionError` and never `UnknownTransactionCommitResult`, when
692
- a fenced `_id` has a committed change newer than the transaction's snapshot:
693
- an insert, replace, update, delete or TTL expiry, by any writer, inside or
694
- outside a transaction. Fencing an absent `_id` holds its absence. A drop,
695
- rename or re-create of the collection, a drop of its database, and the
696
- creation of a fenced collection or database that was absent also refuse the
697
- commit. The check is atomic with the commit, and nothing of a refused
698
- transaction becomes visible; `withTransaction` retries it on a fresh
699
- snapshot.
700
- - A fence can also name documents by the value of a unique key, which holds
701
- a value whether a document holds it or not:
702
- `{ readFence: <collection>, key: <field>, values: [...] }`. On an existing
703
- collection the field needs a unique, non-partial, single-field index with
704
- the simple collation, otherwise the statement fails with `IndexNotFound`
705
- (27). A null value, empty `values`, more than 1,000 values, or `ids` and
706
- `key` together fail with `BadValue` (2); equal values count once.
707
- `commitTransaction` is refused when, after the snapshot, a committed write
708
- changed the document that held a fenced value, or made any document hold
709
- one: an insert of an absent value, an update or replace of the holder, its
710
- delete, a move of its key to another value, a delete and re-insert, another
711
- document taking the value, or TTL expiry. Dropping the index also refuses
712
- the commit. `key: "_id"` is the fence by `ids`.
713
- - The statement itself never fails because of a concurrent change: only the
714
- commit decides.
715
- - A fence takes no write claim. Fences never conflict with each other, never
716
- wait for a writer and never make one wait or fail. A writer that commits
717
- after the fencer has committed succeeds: the order is fencer, then writer.
718
- Changes to other documents of the collection, new indexes and new
719
- collections do not refuse the fencer.
720
- - Fencing and writing the same document makes the transaction a plain writer
721
- of it. A transaction that only reads and fences still gets the commit check.
722
- Transactions without fences behave exactly as before.
723
- - The statement takes the database gate every transaction statement takes, so
724
- `drop`, `renameCollection` and `dropDatabase` in that database wait for the
725
- transaction to end, as they wait for any transaction with a statement there.
726
- It retains no collection gate: creating the fenced collection or its indexes
727
- does not wait for the transaction.
727
+ their statements and the second commit reports `WriteConflict` (112),
728
+ implemented to be MongoDB compatible.
728
729
 
729
730
  Write intents live in memory only; no transaction survives a restart, so
730
731
  there is nothing to recover.
@@ -744,15 +745,6 @@ options. A new collection or a changed index catalog waits only for the
744
745
  transactions that named that collection, and revalidates under its exclusive
745
746
  collection gate before any publication (see "Database waits" below).
746
747
 
747
- Logical sessions survive socket disconnects. Expiry, `endSessions` and
748
- `killSessions` drain active work and release cursors and namespace gates.
749
- A `commitTransaction` or `abortTransaction` retried while its session is being
750
- ended still replays the transaction's stored outcome, so a committed
751
- transaction is never reported as aborted; a transaction that was still open
752
- when its session ended publishes nothing and reports `NoSuchTransaction` (251).
753
- Rejected new transaction envelopes do not consume session capacity or reset
754
- existing sessions. See the native resource budgets below.
755
-
756
748
  ### Durable Committed Writes
757
749
 
758
750
  `NoSqlDbServer.getCommittedWrites()` and the database-scoped wire command
@@ -789,13 +781,16 @@ an application write; subsequent serving publications start retained coverage
789
781
  after that boundary. A move preserving the provider identity preserves the
790
782
  cursor epoch, but any uncovered native publication still reports a gap.
791
783
 
792
- Opening a 10.8.1 root does not initialize a journal or rewrite any stored record.
793
- The first new serving publication atomically adds optional journal and coverage
794
- records, beginning at the old committed head; earlier history explicitly gaps.
795
- This is additive lazy journal initialization, not a historical rewrite. Engines
796
- before 10.9 do not recognize the added record families, so do not downgrade a
797
- root after 10.9 writes. A deployment pinned to 10.8.1 remains unaffected unless
798
- its storage root is deliberately opened for writes with the new engine.
784
+ A start converts a root that a 10.x release wrote, a 10.8.1 root included, into
785
+ this release's format (see **Storage format migration** under File startup
786
+ admission). The converted root keeps the 10.x root's committed position as its
787
+ head and carries no history of writes committed before the conversion: a cursor
788
+ before that head gets `gap: true`, and publications after the conversion are
789
+ retained from that head on. No earlier release reads the converted root. A
790
+ deployment pinned to 10.8.1 is unaffected until a start of this release opens
791
+ its root; keep a stopped copy of the root before that if you may need to go
792
+ back. For a root that holds auth users, a copy restored into another directory
793
+ is no way back (see **Storage format migration** under File startup admission).
799
794
 
800
795
  Pages are exclusive of `after` and follow commit order. Their `cursor` advances
801
796
  over examined publications even when none belongs to the requested database;
@@ -882,7 +877,7 @@ const released = await server.commitDatabasePublication({
882
877
  });
883
878
  ```
884
879
 
885
- The held barrier survives restart and blocks wire commands, transactions and index restoration for that database while unrelated databases remain available. A blocked wire command fails with `ConflictingOperationInProgress` (code 117, no error label) naming `EFENCE_PUBLICATION_HELD`. `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.
880
+ The held barrier survives restart and blocks wire commands, transactions and index restoration for that database while unrelated databases remain available. A blocked wire command fails with `ConflictingOperationInProgress` (code 117, no error label) naming `EFENCE_PUBLICATION_HELD`. `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. 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.
886
881
 
887
882
  Durable database publication holds and allocation fencing use native file storage on the supported Linux and macOS filesystems.
888
883
 
@@ -892,7 +887,7 @@ Coordinators that do not yet have their own durable record can call `getDatabase
892
887
 
893
888
  `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.
894
889
 
895
- 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; any other principal is refused with `Unauthorized` (code 13), and while the allocation is not active every wire command on the database is refused with `Unauthorized` naming `EALLOCATION_DEPROVISIONED`. The database/user ownership-changing wire commands `dropDatabase`, `createUser`, `updateUser`, `dropUser`, `grantRolesToUser` and `revokeRolesFromUser` are refused with `Unauthorized` (code 13), as MongoDB 8 refuses a user without the privilege.
890
+ 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; any other principal is refused with `Unauthorized` (code 13), and while the allocation is not active every wire command on the database is refused with `Unauthorized` naming `EALLOCATION_DEPROVISIONED`. The database/user ownership-changing wire commands `dropDatabase`, `createUser`, `updateUser`, `dropUser`, `grantRolesToUser` and `revokeRolesFromUser` are refused with `Unauthorized` (code 13), the MongoDB-compatible refusal of a user without the privilege.
896
891
 
897
892
  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.
898
893
 
@@ -996,7 +991,7 @@ await client.db('admin').command({ usersInfo: 'reader' });
996
991
  | Method / Property | Type | Description |
997
992
  |---|---|---|
998
993
  | `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 |
999
- | `stop()` | `Promise<void>` | Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup |
994
+ | `stop()` | `Promise<INoSqlDbServerStopResult>` | Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup; resolves with how the engine ended |
1000
995
  | `getConnectionUri()` | `string` | Get the active `mongodb://` URI; before start and after stop, port `0` remains unresolved |
1001
996
  | `running` | `boolean` | Whether the server is currently running |
1002
997
  | `port` | `number` | Actual bound port while running; otherwise the configured port (TCP mode) |
@@ -1024,6 +1019,30 @@ await client.db('admin').command({ usersInfo: 'reader' });
1024
1019
  | `deleteDatabaseTenant(params, options?)` | `Promise<INoSqlDbDeleteDatabaseTenantResult>` | Durably delete an exact tenant database/user, optionally leaving publication held; supports fail-stop cancellation and deadlines |
1025
1020
  | `commitDatabasePublication(params, options?)` | `Promise<TNoSqlDbCommitDatabasePublicationResult>` | Idempotently release an exact held publication receipt; supports fail-stop cancellation and deadlines |
1026
1021
 
1022
+ `stop()` resolves with an `INoSqlDbServerStopResult` that says how the engine
1023
+ ended: `stopRequestSent`, whether a stop request was sent to a running
1024
+ engine; `stopAcknowledged`, whether the engine acknowledged it;
1025
+ `forcedByServer`, whether the server had to signal or kill the engine to end
1026
+ it; and `exitCode` and `exitSignal`, as the engine process's exit reported
1027
+ them, each `null` where there was none. `clean` is true exactly when the
1028
+ request was sent and acknowledged, the server neither signalled nor killed the
1029
+ engine, and the engine exited with code 0 and no signal. After an acknowledged
1030
+ request, `stop()` closes the engine's management input and gives the engine
1031
+ `noSqlDbServerStopGraceMs` (10,000 ms) to exit by itself; only an engine that
1032
+ has not exited by then, or one that did not acknowledge, is ended by the
1033
+ server. A stop when no engine runs resolves with a result that says no request
1034
+ was sent, and concurrent callers share one result. `stop()` rejects only when
1035
+ the bridge's termination fails, as before. `LocalNoSqlDb.stop()` resolves
1036
+ with its server's result.
1037
+
1038
+ A start that meets a storage root, or a feature of one, from a later NoSQLDB
1039
+ release than the one starting refuses it before it writes anything there and
1040
+ creates no root in its place. `start()` of `NoSqlDbServer` and of
1041
+ `LocalNoSqlDb` then rejects with a `NoSqlDbUnknownRootError`, whose `code`
1042
+ is `UNKNOWN_ROOT` and whose message names that cause; its `cause` is the
1043
+ management error that carried the code. A caller tells this refusal apart
1044
+ from every other start failure by the code, never by the message.
1045
+
1027
1046
  ### NoSqlDbStorageRehearsal
1028
1047
 
1029
1048
  `NoSqlDbStorageRehearsal.createDestination(input, options?)` creates an isolated,
@@ -1113,11 +1132,14 @@ publication and validated on retry. Every other record's bytes and version are
1113
1132
  preserved, with unchanged complete non-profile record count and fingerprint.
1114
1133
  Its authority admits no internal relocation or alignment workspace. An
1115
1134
  unrelated staged workspace rejects before native opening, append or recovery.
1116
- Existing v2 rehearsal journals without a plan remain readable: an unfinished
1117
- binding intent explicitly migrates to the v3 journal and acquires a plan before
1118
- its first append, while an already-bound
1119
- stage is verified without rebinding or adding a second commit. Existing native
1120
- manifests, fencing-profile schemas and committed records need no rewrite.
1135
+ This release neither rehearses a root that a 10.x release wrote nor finishes
1136
+ a rehearsal that a 10.x release began, or returns the receipt of one that it
1137
+ finished. A request whose source, destination or unfinished copy is such a
1138
+ root, in whole or in part, fails with `UNSUPPORTED`, and the retry of one
1139
+ whose 10.x roots starts have converted since fails with `UNSUPPORTED` or
1140
+ `SOURCE_CHANGED`; no such request changes either path or the operation
1141
+ workspace. Finish a 10.x rehearsal with that release before a start converts
1142
+ its source, or rehearse the converted root into another destination.
1121
1143
 
1122
1144
  Active source owners, rehearsal-as-source, old or mixed layouts, unknown entries,
1123
1145
  unfinished import or fencing operations, active read grants, unfinished or
@@ -1220,7 +1242,7 @@ underlying server and bridge are constructed by `LocalNoSqlDb.start()`.
1220
1242
  | Method / Property | Type | Description |
1221
1243
  |---|---|---|
1222
1244
  | `start()` | `Promise<ILocalNoSqlDbConnectionInfo>` | Start and return connection info |
1223
- | `stop()` | `Promise<void>` | Stop the server |
1245
+ | `stop()` | `Promise<INoSqlDbServerStopResult>` | Stop the server; resolves with its server's stop result |
1224
1246
  | `getConnectionInfo()` | `ILocalNoSqlDbConnectionInfo` | Get current connection info |
1225
1247
  | `getConnectionUri()` | `string` | Get the connection URI |
1226
1248
  | `getServer()` | `NoSqlDbServer` | Access the underlying server |
@@ -1288,9 +1310,10 @@ source, inspect it for pins, copy it while it remains stopped, and align the cop
1288
1310
  before its first start. Pins cannot prove that an unverified backup is complete;
1289
1311
  a file truncated exactly at a commit boundary can still be internally consistent.
1290
1312
 
1291
- Version 1 requires current production file storage, a clean committed tail, no
1292
- generation newer than the manifest, no auth users, and no fencing, allocation,
1293
- publication-hold, grant or import-stage authority. The complete catalog, auth,
1313
+ Version 1 requires current production file storage, a clean committed tail that
1314
+ no serving open would still repair, no auth users, and no fencing, allocation,
1315
+ publication-hold, grant or import-stage authority. A root that a 10.x release
1316
+ wrote is refused; a start converts it first. The complete catalog, auth,
1294
1317
  index, transaction-outcome and fencing audits must pass. Rehearsals, memory
1295
1318
  storage, unknown files (including `.DS_Store`/AppleDouble), symlinks, hard links
1296
1319
  and unsafe ownership or modes are refused. The operation never repairs them.
@@ -1323,8 +1346,9 @@ existing file is never overwritten. Use the relocation identifier rules for
1323
1346
  `acknowledgeUnverifiedCopy: true` when no source pins survive, never both.
1324
1347
  `binaryPath`, `timeoutMs` and `signal` select the engine and bound the operation.
1325
1348
 
1326
- The following archive recipe was qualified from macOS arm64 APFS to Linux
1327
- x86_64 ext4 and XFS. Run as the service user, with the source stopped throughout,
1349
+ The following archive recipe was qualified on this release's format-3 roots,
1350
+ from macOS arm64 APFS to Linux x86_64 ext4 and XFS (alignment.md records the
1351
+ run). Run as the service user, with the source stopped throughout,
1328
1352
  and retain its inspection pins separately. Start with a new private destination
1329
1353
  parent; preserve the private file/directory modes and do not include filesystem
1330
1354
  metadata sidecars. On macOS use the exact canonical case and Unicode spelling;
@@ -1365,16 +1389,16 @@ include `INVALID_REQUEST`, `NOT_FOUND`, `DESTINATION_CONFLICT`, `STATE_CORRUPT`,
1365
1389
  `abortStoppedStorageRootAlignment(request, options?)` accepts the identical
1366
1390
  request and only rolls back an exact **incomplete** planned append. A complete
1367
1391
  append must finish alignment. If permissions changed after interruption, restore
1368
- the required ownership and modes first. Abort permits changed timestamps while
1369
- still checking original inodes, the full inventory, original bytes and append
1370
- prefix. It durably records abort, truncates/fsyncs, removes its exact reserved
1371
- receipt and cleans up. Retry abort after an interrupted abort; alignment cannot
1372
- override that decision. An absent workspace returns `not-pending`.
1373
-
1374
- Older 8.0.2 engines reject pending workspaces with “startup rejects existing
1375
- file-provider bookkeeping”. Finish or abort with the newer engine; deleting the
1376
- workspace is not a fix. A v8-produced fixture remains readable by v8 after this
1377
- one-profile append. This does **not** allow downgrading v10-created catalogs.
1392
+ the required ownership and modes first. Abort permits a changed change time,
1393
+ which restoring ownership or modes leaves, while still checking every file's
1394
+ identity, length and modification time, the original bytes, and that the planned
1395
+ append's place holds only its own bytes or zeros. It durably records abort,
1396
+ writes zeros where the interrupted append wrote and syncs, removes its exact
1397
+ reserved receipt and cleans up. Retry abort after an interrupted abort; alignment
1398
+ cannot override that decision. An absent workspace returns `not-pending`.
1399
+
1400
+ Finish or abort a pending alignment with this release; deleting the workspace is
1401
+ not a fix. No earlier release reads a root that this release has aligned.
1378
1402
  Qualification results and study corrections are recorded in the repository's
1379
1403
  [alignment design note](https://code.foss.global/lossless.org/nosqldb/src/branch/main/alignment.md).
1380
1404
 
@@ -1387,7 +1411,14 @@ through the complete record-owner audit. A running engine returns `BUSY`.
1387
1411
 
1388
1412
  An `audited` result includes the recorded provider identity, live binding state,
1389
1413
  lineage (`groupId`/`timelineId`), decimal-string LSN and counts, full-record and
1390
- profile-excluded SHA-256 digests, generation inventory and alignment eligibility.
1414
+ profile-excluded SHA-256 digests, `storageFormat: 'format-3'` for a root of this
1415
+ release or the generation inventory of a root that a 10.x release wrote, and
1416
+ alignment eligibility. A format-3 root that a serving open would still repair,
1417
+ such as one whose log ends inside an unfinished write, is ineligible with
1418
+ `RECOVERY_PENDING`. A root that a 10.x release wrote is audited read-only as that
1419
+ release audited it, with the same identity, counts, digests and generation
1420
+ inventory, and is ineligible with `CONVERSION_PENDING` first among its reasons:
1421
+ this release does not align it, and a start converts it first.
1391
1422
  It counts allocation replays as well as fences, receipts, holds and allocations.
1392
1423
  The audit accepts no active grants or import stages. Auth users or authority
1393
1424
  history make a fully audited root ineligible for independent-fork alignment.
@@ -1407,10 +1438,15 @@ the TypeScript interfaces for older engine binaries.
1407
1438
  #### Offline String-Value Inspection
1408
1439
 
1409
1440
  `LocalNoSqlDb.inspectOfflineStringValue(input, options?)` reads one string from a
1410
- stopped current native file root on supported Linux filesystems or macOS APFS.
1441
+ stopped native file root of this release, or read-only from one that a 10.x
1442
+ release wrote, on supported Linux filesystems or macOS APFS.
1411
1443
  It starts only a disposable management sidecar, acquires the existing native
1412
1444
  leases read-only, and pins the committed root. It never starts a listener,
1413
- initializes storage, repairs tails, replays writes or compacts the source.
1445
+ initializes storage or writes to the source: a root of this release is
1446
+ recovered in memory only, as a start would recover it, and a cut log tail or a
1447
+ leftover anchor replacement stays on disk as found. A root that a 10.x release
1448
+ wrote is read up to its committed state, and one that a 10.x process left
1449
+ without a clean stop is refused with `RECOVERY_REQUIRED`.
1414
1450
 
1415
1451
  ```typescript
1416
1452
  import { LocalNoSqlDb } from '@lossless.org/nosqldb';
@@ -1501,7 +1537,7 @@ await debugServer.start();
1501
1537
  await debugServer.stop();
1502
1538
  ```
1503
1539
 
1504
- The UI is bundled as base64-encoded content (via `@git.zone/tsbundle`) and served from memory — no static file directory needed.
1540
+ The UI is bundled as base64-encoded content (via `tsrepo bundle` from `@git.zone/tsrepo`) and served from memory — no static file directory needed.
1505
1541
 
1506
1542
  ### NoSqlDbDebugUi (Web Component)
1507
1543
 
@@ -1553,72 +1589,20 @@ await collection.findOneAndReplace({ _id: id }, { name: 'Replaced' }, { returnDo
1553
1589
 
1554
1590
  ### Query Operators
1555
1591
 
1556
- ```typescript
1557
- // Comparison
1558
- { age: { $eq: 25 } } { age: { $ne: 25 } }
1559
- { age: { $gt: 18 } } { age: { $lt: 65 } }
1560
- { age: { $gte: 18 } } { age: { $lte: 65 } }
1561
- { status: { $in: ['active', 'pending'] } }
1562
- { status: { $nin: ['deleted'] } }
1563
-
1564
- // Logical
1565
- { $and: [{ age: { $gte: 18 } }, { active: true }] }
1566
- { $or: [{ status: 'active' }, { admin: true }] }
1567
- { $not: { status: 'deleted' } }
1568
-
1569
- // Element
1570
- { email: { $exists: true } }
1571
- { type: { $type: 'string' } } { value: { $type: ['int', 'long'] } }
1572
-
1573
- // Array
1574
- { tags: { $all: ['mongodb', 'database'] } }
1575
- { scores: { $elemMatch: { $gte: 80, $lt: 90 } } }
1576
- { labels: { $not: { $elemMatch: { $nin: ['linux', 'gpu'] } } } }
1577
- { items: { $elemMatch: { sku: 'a', qty: { $gt: 2 } } } }
1578
- { items: { $all: [{ $elemMatch: { sku: 'a' } }, { $elemMatch: { qty: 0 } }] } }
1579
- { tags: { $size: 3 } }
1580
-
1581
- // Regex
1582
- { name: /^Al/i } { name: { $regex: '^al', $options: 'i' } }
1583
- { tags: { $in: [/^db/, 'mongodb'] } }
1584
-
1585
- // Aggregation expressions
1586
- { owner: 'o', $expr: { $lte: [{ $add: ['$retainedBytes', '$reservedBytes', size] }, max] } }
1587
- { $expr: { $gt: ['$leaseExpiresAt', '$$NOW'] } }
1588
- ```
1589
-
1590
- `$type` takes a type alias (`'string'`, `'number'`, …), a numeric BSON type
1591
- code or an array of them, and matches an array field through its elements as
1592
- well as `'array'` itself.
1593
-
1594
- `$comment` annotates a filter without constraining it, at the top level and
1595
- in an `$and`, `$or`, `$nor` or `$elemMatch` document; under a field it is
1596
- refused as an unknown operator, as on MongoDB.
1597
-
1598
- A filter is checked as MongoDB 8 checks it, before any document is read and
1599
- with its messages: `$and`, `$or` and `$nor` need a non-empty array of
1600
- documents, `$in`, `$nin` and `$all` an array, and a `$in` or `$nin` member
1601
- whose first field starts with `$`, such as `{ $gt: 1 }`, is refused
1602
- (`cannot nest $ under $in`), each with `BadValue` (2). A DBRef member such as
1603
- `{ $ref: 'users', $id: id }` is a value. Partial index filters and `$pull`
1604
- conditions refuse the query features MongoDB 8 bans there: `$expr` and
1605
- `$jsonSchema` with `QueryFeatureNotAllowed` (224), `$where` and `$text` with
1606
- `BadValue`, and `$near`, `$nearSphere` and `$geoNear` with `Location5626500`.
1607
-
1608
- `$elemMatch` has MongoDB's two forms. An operand whose first key is a query
1592
+ `$elemMatch` has two forms, implemented to be MongoDB compatible. An operand whose first key is a query
1609
1593
  operator applies its operators (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`,
1610
1594
  `$in`, `$nin`, `$exists`, `$type`, `$regex`, `$size`, `$mod`, `$all`, `$not`,
1611
1595
  `$elemMatch`) to each element as a whole; any other operand is a query over
1612
1596
  each embedded document, and a nested array is matched as a document keyed by
1613
1597
  index. A dotted path tests every array it reaches on its own. `$expr` inside
1614
- `$elemMatch` is refused with `BadValue` (2), as on MongoDB.
1598
+ `$elemMatch` is refused with `BadValue` (2), implemented to be MongoDB compatible.
1615
1599
 
1616
1600
  A regular expression matches the same way wherever a filter holds one — as a
1617
1601
  field's value, through `$regex` and `$options`, in a `$in`, `$nin` or `$all`
1618
1602
  list, under `$not`, and as a `$pull` condition: a string or symbol matches when
1619
1603
  the pattern finds a match in it, an array matches through its elements, a
1620
1604
  stored regular expression matches only when its pattern and options are equal,
1621
- and a missing field never matches. `$options` takes MongoDB's flags `i`, `m`,
1605
+ and a missing field never matches. `$options` takes the MongoDB-compatible flags `i`, `m`,
1622
1606
  `s`, `x` and `u`. Patterns run on Rust's `regex` crate rather than PCRE, so
1623
1607
  matching takes time linear in the input, constructs only PCRE has
1624
1608
  (look-around, backreferences, `\Q…\E`, `\Z`, `(?#…)` comments, a `{` that
@@ -1627,28 +1611,16 @@ starts no repetition) are refused as invalid, and a few match differently:
1627
1611
  does not match before a final newline, a possessive quantifier such as `a++`
1628
1612
  is greedy, and `x` ignores whitespace inside a character class too. A
1629
1613
  pattern is checked when the filter is validated, before any document is read
1630
- and even when the collection does not exist, with MongoDB's errors in
1631
- MongoDB's order: options given in both `$regex` and `$options` are
1614
+ and even when the collection does not exist, with MongoDB-compatible errors in
1615
+ a MongoDB-compatible order: options given in both `$regex` and `$options` are
1632
1616
  `Location51074` or `Location51075`, an unknown flag `Location51108`, a null
1633
1617
  byte `BadValue` (2), a pattern longer than 32,764 bytes or one that does not
1634
1618
  compile `Location51091` `Regular expression is invalid: <reason>` (the reason
1635
- is the crate's, such as `unclosed group` where MongoDB says `missing closing
1619
+ is the crate's, such as `unclosed group` where MongoDB, as observed by runtime testing, answers `missing closing
1636
1620
  parenthesis`), and a regular expression as the operand of `$ne`, `$gt`,
1637
1621
  `$gte`, `$lt` or `$lte` `BadValue` (2). A match charges the work budget the
1638
1622
  size of the field's value times the length of the pattern.
1639
1623
 
1640
- `$expr` evaluates an aggregation expression per document in `find`, `count`,
1641
- `distinct`, `update`, `delete`, `findAndModify` and a `$match` stage, and
1642
- matches when the result is truthy. It accepts the operators listed under
1643
- [Aggregation Pipeline](#aggregation-pipeline). Evaluation is charged against
1644
- the same work budget as every other predicate, and an operator the engine does
1645
- not implement is refused by name with `TypeMismatch` (code 14) before any
1646
- document is read. `$$NOW` is one instant per command, as in MongoDB: the
1647
- engine reads the clock once when the command starts, and the command's filter,
1648
- every document of a pipeline update, every aggregation stage, `$lookup` and
1649
- `$unionWith` sub-pipeline, and every `getMore` of its cursor see that instant.
1650
- Filters outside a command, such as a partial index filter, have no `$$NOW`.
1651
-
1652
1624
  ### Update Operators
1653
1625
 
1654
1626
  ```typescript
@@ -1671,8 +1643,8 @@ may be an array of `$addFields`, `$set`, `$project`, `$unset`, `$replaceRoot`
1671
1643
  or `$replaceWith` stages, and any other stage is refused by name. The pipeline
1672
1644
  must produce exactly one document and cannot change `_id`.
1673
1645
 
1674
- An upsert that matches nothing inserts the filter's equality conditions, as
1675
- MongoDB does: top-level fields and `$and` clauses, `$eq`, a single-element
1646
+ An upsert that matches nothing inserts the filter's equality conditions, to be
1647
+ MongoDB compatible: top-level fields and `$and` clauses, `$eq`, a single-element
1676
1648
  `$in`, `$all` members and a single-clause `$or`. A dotted path such as
1677
1649
  `'owner.id'` becomes a nested document, and a regular expression is copied only
1678
1650
  as an `$eq` operand. A replacement takes only `_id` from the filter and fails
@@ -1695,64 +1667,6 @@ const results = await collection.aggregate([
1695
1667
 
1696
1668
  **Supported stages:** `$match`, `$project`, `$group`, `$sort`, `$limit`, `$skip`, `$unwind`, `$lookup`, `$addFields`, `$count`, `$facet`, `$replaceRoot`, `$set`, `$unionWith`, `$out`, `$merge`
1697
1669
 
1698
- **Supported expression operators:**
1699
-
1700
- | Group | Operators |
1701
- | --- | --- |
1702
- | Comparison | `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$cmp` |
1703
- | Arithmetic | `$add`, `$subtract`, `$multiply`, `$divide` |
1704
- | Logical | `$and`, `$or`, `$not` |
1705
- | Conditional and variables | `$cond` (array or `if`/`then`/`else` form), `$ifNull` (two or more operands), `$let` |
1706
- | Type and arrays | `$isNumber`, `$type`, `$in`, `$size`, `$arrayElemAt` |
1707
- | Dates | `$dateAdd` |
1708
- | Collections | `$map`, `$objectToArray`, `$setEquals` |
1709
- | Literals | `$literal` |
1710
-
1711
- plus `$field` paths, `$$variable` references including `$$ROOT` and `$$NOW`,
1712
- and documents or arrays of those. Every other operator is refused by name with
1713
- `TypeMismatch` (code 14) before any document is read; the engine never treats
1714
- an operator it cannot execute as a literal value.
1715
-
1716
- Expressions follow MongoDB's value semantics. A field that is absent is
1717
- *missing*, which is not `null`: `$type` answers `'missing'`, `$ifNull`
1718
- replaces both, and comparisons order values as MongoDB does (MinKey, missing,
1719
- `null`, numbers, strings, objects, arrays, binary data, ObjectIds, booleans,
1720
- dates, timestamps, regular expressions, MaxKey), so `{ $eq: ['$absent', null] }`
1721
- is false. Numbers compare by value across their wire types. A path through an
1722
- array maps over its elements and always answers an array: `'$mailbox.path'`
1723
- over one looked-up document is `['INBOX']`, so `$arrayElemAt` and `$size`
1724
- apply to it. Arithmetic answers `null` when an operand is `null` or missing;
1725
- an int result that overflows widens to a long and a long to a double, a double
1726
- operand makes the result a double, and `$divide` always answers a double.
1727
- `$add` accepts one date and answers a date; date minus date is milliseconds.
1728
- Runtime errors keep MongoDB 7's codes: a non-numeric operand is
1729
- `TypeMismatch` (14), `can't $divide by zero` is `BadValue` (2), a date that
1730
- leaves the 64-bit range is `Overflow` (15), and the array and date operators
1731
- answer MongoDB's location codes (a second date in `$add` 16612, `$in` without
1732
- an array 40081, `$size` without an array 17124, `$arrayElemAt` 28689–28691,
1733
- `$dateAdd` 5166403, 5166405, 5166406 and 5439013, a non-string time zone
1734
- 40517, an unknown unit `FailedToParse` (9)). Decimal128 operands in arithmetic
1735
- are refused with `TypeMismatch` (14) — decimal arithmetic is not
1736
- implemented. `$and`, `$or`, `$cond` and `$ifNull`
1737
- evaluate, and charge, only the operands MongoDB evaluates, so a guard keeps an
1738
- invalid branch from failing the statement. `$dateAdd` computes in UTC with the
1739
- units `millisecond` to `year`, clamping `month`, `quarter` and `year` to the
1740
- end of the target month; a `timezone` other than UTC is refused by name.
1741
- Within a document expression a member that is missing is still written as
1742
- `null`, where MongoDB omits the member.
1743
-
1744
- `$lookup` supports the equality form and a bounded correlated pipeline form with
1745
- `let`, an optional `$match` (any filter, `$expr` included), and an optional
1746
- following `$limit`. A `let` variable that reads an absent local field is bound
1747
- to the missing value, as in MongoDB. Correlated string equality predicates
1748
- (`$expr` with `$eq`, optionally under `$and`) can use a full-key or subset
1749
- foreign equality index to reduce candidates when available. Candidates are
1750
- paged, the complete expression remains authoritative, and `$limit` counts only
1751
- exact expression matches. Other BSON value shapes use the bounded scan path.
1752
- Mixed `localField`/`foreignField` plus `pipeline` lookups and other inner stages
1753
- are rejected explicitly. Complete filter trees are validated before data is
1754
- read, and query nesting plus aggregation work are bounded.
1755
-
1756
1670
  A leading `$match` narrows the pipeline's source load instead of filtering
1757
1671
  after it: only matching documents are materialized, using an equality index when
1758
1672
  one serves the filter and a bounded scan otherwise. Filter shapes the query
@@ -1772,19 +1686,6 @@ describes, as it does on its own.
1772
1686
 
1773
1687
  **Group accumulators:** `$sum`, `$avg`, `$min`, `$max`, `$first`, `$last`, `$push`, `$addToSet`, `$count`
1774
1688
 
1775
- `$group._id` accepts a literal (`null`, a number, a string), a `$field`
1776
- reference, or a document of expressions: `{ _id: { owner: '$owner', repo:
1777
- '$repo' } }` groups by the evaluated pair, one group per distinct composite
1778
- key. Inside a composite `_id`, a member whose field is absent evaluates to
1779
- `null` and stays part of the key, where MongoDB omits the member instead.
1780
- Accumulator operands accept the same expression forms. A field reference
1781
- resolves per document, and a missing field is not a value: `$sum`, `$avg`,
1782
- `$min`, `$max`, `$push` and `$addToSet` skip it, while `$first` and `$last`
1783
- record `null` for it. Every field beside `_id` must hold an accumulator
1784
- document naming exactly one accumulator, such as `{ $sum: 1 }`, and an operator
1785
- outside the two lists above is refused with `TypeMismatch` (code 14) when the
1786
- stage is parsed.
1787
-
1788
1689
  ### Indexes
1789
1690
 
1790
1691
  ```typescript
@@ -1798,77 +1699,8 @@ await collection.dropIndexes(); // drop all except _id
1798
1699
 
1799
1700
  > 🛡️ **Unique indexes are enforced at the engine level.** Duplicate values are rejected with a `DuplicateKey` error (code 11000) *before* the document is written to disk — on `insertOne`, `updateOne`, `findAndModify`, and upserts. Index definitions and entries are committed with native storage and survive restart.
1800
1701
 
1801
- NoSQLDB supports ascending and descending index keys with `name`, `unique`,
1802
- `sparse`, `partialFilterExpression` and `expireAfterSeconds` options. It
1803
- accepts `background` as a validated no-op and index version `v: 2`.
1804
- Unsupported options, including `collation` and `hidden`, are rejected before
1805
- any index in the request is created. A legacy `ns` field is accepted only when
1806
- it names the target namespace. With `ignoreUnknownIndexOptions: true`,
1807
- `createIndexes` removes the fields MongoDB 7 does not know as index options
1808
- (and plugin-specific fields such as `weights` or `bits` on an ascending or
1809
- descending key) before validation, as MongoDB does; options MongoDB knows but
1810
- NoSQLDB does not implement are still rejected. Native maintenance expires eligible BSON
1811
- dates and date arrays in bounded batches, preserving publication holds and
1812
- active snapshots.
1813
-
1814
- **Partial indexes.** An index with a `partialFilterExpression` holds only the
1815
- documents that match the filter, and a unique partial index enforces
1816
- uniqueness among those documents only:
1817
-
1818
- ```typescript
1819
- await jobs.createIndex(
1820
- { activeClaimKey: 1 },
1821
- { unique: true, partialFilterExpression: { activeClaimKey: { $exists: true } } },
1822
- );
1823
- await pulls.createIndex(
1824
- { org: 1, repo: 1, sourceRef: 1, targetRef: 1 },
1825
- { unique: true, partialFilterExpression: { state: 'open' } },
1826
- );
1827
- ```
1828
-
1829
- The filter accepts MongoDB 8's subset: equality (`field: value` or `$eq`),
1830
- `$exists: true`, `$gt`, `$gte`, `$lt`, `$lte`, `$type`, `$in`, whose list may
1831
- hold regular expressions that match as they do in a query, `$all` of values,
1832
- which is the conjunction of their equalities and counts as one level, `$and`
1833
- and `$or`, nested at most four expression levels deep; `$comment` is accepted
1834
- and ignored. Any other expression — `$exists: false`, `$ne`, `$nin`, `$not`,
1835
- `$nor`, a regular expression outside `$in`, an empty `$all` or one holding a
1836
- regular expression or `$elemMatch`, `$elemMatch` and `$size` — and a filter
1837
- combined with `sparse` are refused with `CannotCreateIndex` (code 67);
1838
- `$expr`, `$jsonSchema`, `$where`, `$text` and the `$near` operators are
1839
- refused with the codes listed under [Query Operators](#query-operators), a
1840
- malformed filter, such as an empty `$and`, is `BadValue` (2) and a
1841
- non-document is `TypeMismatch` (14). As in MongoDB, the whole filter is first
1842
- validated as a query, so a malformed operand or an invalid regular expression
1843
- anywhere in it (`Location51091`, `Location51108`) is answered before an
1844
- expression outside the subset.
1845
- An update that moves a document into the filter claims its key, one that moves
1846
- it out releases it, and transactions claim keys the same way, so a racing
1847
- claim is a `WriteConflict` (112) with `TransientTransactionError`.
1848
- `listIndexes`, canonical export and import report the filter exactly as it was
1849
- created. The filter identifies the index: the same name with a different
1850
- filter is `IndexKeySpecsConflict` (86); the same key with an equivalent filter
1851
- under another name is `IndexOptionsConflict` (85); a different filter over the
1852
- same key under another name is a separate index. Two filters are equivalent
1853
- when MongoDB 8 normalises them to the same expression: implicit equality and
1854
- `$eq`, numeric wire types, `$in: [1]` and `1`, an `$or` of equalities on one
1855
- path and their `$in`, `$in` order and duplicates, `$all` and the `$and` of
1856
- its values, nested and flat `$and` or `$or`, `$type` aliases and their codes,
1857
- `$exists: true` and `1`, and a `$comment` or none compare equal, and so do
1858
- conjuncts and `$or` branches on different paths or of different kinds in any
1859
- order. Predicates of one kind on one path keep their written order, so
1860
- `{ $and: [{ a: 1 }, { a: 2 }] }` and its reverse are separate indexes, as are a
1861
- repeated conjunct and the conjunct alone, a document operand with its fields
1862
- in another order, and `$type: 'number'` and the list of the four numeric
1863
- types. The query
1864
- planner never selects a partial index on its own; a `hint` naming one reads
1865
- only its members, as MongoDB's hinted scan does. A partial TTL index expires
1866
- only its members. Engines older than this release refuse a storage root that
1867
- holds a partial index instead of enforcing its uniqueness over every
1868
- document.
1869
-
1870
1702
  **List fields.** An index on a field that holds lists has an entry per element,
1871
- as a MongoDB multikey index, and a compound index an entry per combination of
1703
+ to be compatible with MongoDB multikey indexes, and a compound index an entry per combination of
1872
1704
  its list fields' elements. Equality, `$in` and one-sided ranges on the elements
1873
1705
  read their bounds, and a document with several entries within them is returned
1874
1706
  once. A list matches a range bounded on both sides when one element meets the
@@ -1879,10 +1711,12 @@ through the index. An index with element entries returns no order, so a sorted
1879
1711
  read sorts its matches, and a `hint` naming it is refused. A unique index on a
1880
1712
  list field is unique per element: a document that shares an element with
1881
1713
  another is a `DuplicateKey` (11000), while one document may repeat an element.
1882
- An index whose field already held a list under 10.7.3 or earlier keeps its
1883
- whole-list entries and uniqueness and is maintained but never selected until it
1884
- is dropped and created again. Engines up to 10.7.3 refuse an index with element
1885
- entries instead of maintaining it with whole lists.
1714
+ An index whose field already held a list under 10.7.3 or earlier, which those
1715
+ releases kept with whole-list entries, gets element entries when 11.0.0
1716
+ converts its root at start, and is then selected like any other index; a unique
1717
+ one whose documents share an element refuses the start before anything is
1718
+ written. Engines up to 10.7.3 refuse an index with element entries instead of
1719
+ maintaining it with whole lists.
1886
1720
 
1887
1721
  `find()` and collection `aggregate()` accept `hint` as an index name (including
1888
1722
  `_id_`) or an exact ascending/descending key pattern. Key-pattern order matters;
@@ -1929,7 +1763,7 @@ Nontransaction aggregation pipelines composed of `$match`, `$project`, `$unset`,
1929
1763
  `$set`, `$addFields`, `$replaceRoot`, `$replaceWith`, `$skip`, `$limit`, `$sort`,
1930
1764
  `$group`, `$count`, `$unwind`, `$lookup`, `$unionWith`, and `$facet` also operate
1931
1765
  beyond 10,000 input or output documents.
1932
- Counts use exact collection metadata where applicable, otherwise stream matching
1766
+ Counts use each collection's exact count from its statistics record where applicable, otherwise stream matching
1933
1767
  records. A limit bounds the work needed to find the requested number of matches.
1934
1768
  Without a limit, exact counts continue to EOF. Hints, deadlines and empty-result
1935
1769
  shapes remain supported; signed BSON count overflow and resource exhaustion
@@ -1995,8 +1829,9 @@ transaction and native commit byte admission. `$out` and `$merge` have no
1995
1829
  10,000-document output ceiling; their atomic output is subject to the same write
1996
1830
  budgets and rejects before publication if those budgets are exceeded. Native
1997
1831
  database import uses private staging for larger supported exports, then publishes
1998
- the catalog and receipt atomically. Existing old-engine storage is rejected
1999
- without conversion.
1832
+ the catalog and receipt atomically. A start converts a root that a 10.x release
1833
+ wrote (see **Storage format migration** under File startup admission); any other
1834
+ old storage is rejected without conversion.
2000
1835
 
2001
1836
  ### Comparing performance with MongoDB
2002
1837
 
@@ -2051,40 +1886,23 @@ time-series collections, clustered indexes, encrypted fields, and all general
2051
1886
  view pipelines remain unsupported and are rejected before the database or
2052
1887
  namespace is created.
2053
1888
 
2054
- `create` accepts an `idIndex` only when it describes the primary index every
2055
- collection gets: key `{ _id: 1 }`, name `_id_`, optionally `v: 2` and an `ns`
2056
- naming the new collection. Any other `idIndex` is `InvalidOptions` (72).
2057
-
2058
1889
  **Collection names.** A name with a NUL, an empty name and a name starting
2059
1890
  with `.` that storage cannot hold, such as `.` or `..`, are `InvalidNamespace`
2060
- (73) on every command naming them, with MongoDB 8's messages, before the
2061
- command runs. A name MongoDB 8 accepts that one storage path component cannot
1891
+ (73) on every command naming them, with MongoDB-compatible messages, before the
1892
+ command runs. A name MongoDB accepts that one storage path component cannot
2062
1893
  hold, one with `/`, `\` or a control character or longer than 255 bytes, names
2063
1894
  a collection that cannot exist: reads, `drop`, `viewOn` and a rename source see
2064
1895
  it missing, and commands that create or write it are refused with
2065
1896
  `InvalidNamespace`, or `IllegalOperation` (20) for `$out` or a rename into a
2066
- name longer than 255 bytes. Such a refused write ends its transaction, as
2067
- MongoDB 8's refusal of a namespace longer than 255 bytes does. NoSQLDB limits
2068
- the collection name to 255 bytes where MongoDB 8 limits the whole
1897
+ name longer than 255 bytes. Such a refused write ends its transaction, to be compatible with MongoDB's
1898
+ refusal of a namespace longer than 255 bytes. NoSQLDB limits
1899
+ the collection name to 255 bytes where MongoDB, as observed by runtime testing, limits the whole
2069
1900
  `database.collection`, and it accepts a name such as `.a` that starts with `.`
2070
- but that its storage holds, which MongoDB 8 refuses.
2071
-
2072
- **Importing from MongoDB.** `mongorestore` (MongoDB Database Tools 100.x)
2073
- restores a `mongodump` of ordinary collections with its default flags: the
2074
- documents, validators and supported indexes (unique, compound, sparse,
2075
- partial and TTL). It sends this `idIndex` with every `create` and
2076
- `createIndexes` with each index's `ns` and `ignoreUnknownIndexOptions: true`,
2077
- and it builds several collections' indexes in parallel, which wait for one
2078
- another instead of failing (see "Database waits" below). Exclude the `admin`
2079
- and `config` namespaces (`--nsExclude 'admin.*' --nsExclude 'config.*'`) and create users
2080
- through NoSQLDB's authentication options. Collections or indexes that use
2081
- unsupported features (collation, text, geospatial, hashed or wildcard
2082
- indexes, capped or time-series collections) fail the restore with the
2083
- refusing command's error instead of being restored differently.
1901
+ but that its storage holds, which MongoDB refuses.
2084
1902
 
2085
1903
  ### Collection Validators
2086
1904
 
2087
- `create` and `collMod` accept MongoDB's `validator`, `validationLevel`
1905
+ `create` and `collMod` accept the MongoDB-compatible `validator`, `validationLevel`
2088
1906
  (`strict`, `moderate`, `off`) and `validationAction` (`error`, `warn`), and
2089
1907
  `listCollections` reports them under `options` exactly as set:
2090
1908
 
@@ -2111,7 +1929,7 @@ is `bsonType` (every BSON alias and `number`), `type` (`object`, `array`,
2111
1929
  `additionalProperties` (boolean), `minProperties`, `maxProperties`, `enum`,
2112
1930
  `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `minLength`,
2113
1931
  `maxLength`, `pattern`, `items` (one schema), `minItems`, `maxItems`, `title`
2114
- and `description`, with MongoDB's semantics: a keyword applies only to values
1932
+ and `description`, with MongoDB-compatible semantics: a keyword applies only to values
2115
1933
  of its kind and not at all when `bsonType`/`type` excludes that kind, `enum`
2116
1934
  compares numbers by value and embedded documents regardless of field order,
2117
1935
  NaN fails `minimum` and `maximum`, and lengths count code points. Everything
@@ -2119,9 +1937,9 @@ else is refused when the validator is set, never ignored: `patternProperties`,
2119
1937
  `additionalItems`, `uniqueItems`, `dependencies`, `allOf`, `anyOf`, `oneOf`,
2120
1938
  `not`, `multipleOf`, `encrypt`, `encryptMetadata`, `items` as an array and
2121
1939
  `additionalProperties` as a schema answer `FailedToParse` (9) `$jsonSchema
2122
- keyword '…' is not currently supported`, as MongoDB answers for `$ref`,
1940
+ keyword '…' is not currently supported`, the MongoDB-compatible answer for `$ref`,
2123
1941
  `format`, `default`, `definitions`, `id` and `$schema`; `$jsonSchema` below the
2124
- top level answers `BadValue` (2). Malformed validators answer MongoDB's codes
1942
+ top level answers `BadValue` (2). Malformed validators answer MongoDB-compatible codes
2125
1943
  (2, 9, 14, `Location51091` for an invalid `pattern`). `pattern` uses the same
2126
1944
  regular-expression dialect as `$regex`; constructs only PCRE supports are
2127
1945
  refused as invalid.
@@ -2133,10 +1951,10 @@ written document; `moderate` skips updates to documents that did not satisfy
2133
1951
  the validator before the update; `off` validates nothing; a no-op update and a
2134
1952
  delete are never validated. A refused document answers
2135
1953
  `DocumentValidationFailure` (121), `errmsg: "Document failed validation"`, and
2136
- MongoDB's `errInfo: { failingDocumentId, details }` — as a write error of
1954
+ the MongoDB-compatible `errInfo: { failingDocumentId, details }` — as a write error of
2137
1955
  `insert`/`update`, keeping ordered and unordered batch semantics, and as the
2138
1956
  command error of `findAndModify` and `aggregate`. A refused statement aborts
2139
- its transaction. `details` has MongoDB's shape for every `$jsonSchema` keyword
1957
+ its transaction. `details` has a MongoDB-compatible shape for every `$jsonSchema` keyword
2140
1958
  above and for `$and`, `$or`, `$nor`, `$not`, `$eq`, `$ne`, `$gt`, `$gte`,
2141
1959
  `$lt`, `$lte`, `$exists`, `$type`, `$in`, `$nin`, `$regex`, `$size`, `$all`,
2142
1960
  `$elemMatch`, `$mod` and `$expr`; other operators report only `operatorName`
@@ -2145,12 +1963,12 @@ and `specifiedAs`. `validationAction: 'warn'` writes the document and logs a
2145
1963
  `bypassDocumentValidation` (a boolean or number) on `insert`, `update`,
2146
1964
  `findAndModify` and writing `aggregate` skips validation; with authentication
2147
1965
  it requires `dbAdmin` on the written database, as `collMod` does — `readWrite`
2148
- alone is refused with `Unauthorized` (13), as in MongoDB's built-in roles.
1966
+ alone is refused with `Unauthorized` (13), implemented to be compatible with MongoDB's built-in roles.
2149
1967
 
2150
1968
  `collMod` changes only the options it names; `validator: {}` removes the
2151
1969
  validator; a `null` option is absent; `collMod` without options answers
2152
1970
  `{ ok: 1 }`. A missing collection answers `NamespaceNotFound` (26), a view or
2153
- `system.*` collection `InvalidOptions` (72), MongoDB's other `collMod` options
1971
+ `system.*` collection `InvalidOptions` (72), the other MongoDB `collMod` options
2154
1972
  (`index`, `viewOn`, `pipeline`, `expireAfterSeconds`, `timeseries`,
2155
1973
  `changeStreamPreAndPostImages`, `dryRun`, `cappedSize`, `cappedMax`, …)
2156
1974
  `InvalidOptions` (72) by name, and unknown fields `IDLUnknownField` (40415).
@@ -2163,13 +1981,13 @@ content digest includes them only then, so digests of collections without
2163
1981
  options are unchanged. Engines up to 10.6.1 refuse a storage root or an export
2164
1982
  holding collection options instead of silently not enforcing them.
2165
1983
 
2166
- Differences from MongoDB 7: `create` on an existing collection answers
1984
+ Differences from MongoDB, as observed by runtime testing: `create` on an existing collection answers
2167
1985
  `NamespaceExists` (48) even when the options are identical (MongoDB answers
2168
1986
  `ok`); a validator together with `viewOn` is refused (MongoDB ignores it); an
2169
- invalid `pattern` at the schema root is refused (MongoDB never compiles it
1987
+ invalid `pattern` at the schema root is refused (MongoDB accepts it
2170
1988
  there);
2171
- command errors say `Document failed validation` without MongoDB's
2172
- `Plan executor error during …` prefix; the `warn` log carries `errInfo`, not
1989
+ command errors say `Document failed validation` without the
1990
+ `Plan executor error during …` prefix MongoDB adds; the `warn` log carries `errInfo`, not
2173
1991
  the whole document.
2174
1992
 
2175
1993
  ### Restricted Deny-All Views
@@ -2205,7 +2023,7 @@ replaced by a view keeps that original snapshot for `find`, `count`, and
2205
2023
  with `CommandNotSupportedOnView` (code 166). `dropCollection` is the supported
2206
2024
  DDL exception for removing a logical restricted view.
2207
2025
 
2208
- Definitions are persisted in the physical `system.views` catalog. As in MongoDB,
2026
+ Definitions are persisted in the physical `system.views` catalog. To be MongoDB compatible,
2209
2027
  `listCollections` lists `system.views` as a physical storage collection and the
2210
2028
  logical namespace as type `view` with `{ viewOn, pipeline }`, read-only metadata,
2211
2029
  and no `idIndex`. `system.views` is reserved from direct wire reads,
@@ -2242,9 +2060,6 @@ const estimated = await collection.estimatedDocumentCount();
2242
2060
  const names = await collection.distinct('name');
2243
2061
  ```
2244
2062
 
2245
- The `count` command answers `n` as a 32-bit integer when the count fits and as
2246
- a 64-bit integer above that, as MongoDB does.
2247
-
2248
2063
  ---
2249
2064
 
2250
2065
  ## Wire Protocol Commands
@@ -2256,87 +2071,9 @@ a 64-bit integer above that, as MongoDB does.
2256
2071
  | **Aggregation** | `aggregate`, `count`, `distinct` |
2257
2072
  | **Indexes** | `createIndexes`, `dropIndexes`, `listIndexes` |
2258
2073
  | **Sessions** | `startSession`, `endSessions`, `killSessions` |
2259
- | **Transactions** | `startTransaction`, `commitTransaction`, `abortTransaction` through driver sessions; `readFence` (NoSQLDB-specific, see Read fences) |
2074
+ | **Transactions** | `startTransaction`, `commitTransaction`, `abortTransaction` through driver sessions |
2260
2075
  | **Admin** | `ping`, `buildInfo`/`buildinfo`, `listDatabases`, `listCollections`, `drop`, `dropDatabase`, `create`, `collMod` (validation options), `serverStatus`, `dbStats`, `collStats`, `connectionStatus`, `renameCollection` |
2261
2076
 
2262
- Advertises MongoDB wire protocol versions 0–21 and reports MongoDB 7.0.0
2263
- compatibility for its command surface. The documented command surface is tested
2264
- with the official `mongodb` Node.js driver version 7.5.x. Wire limits and error
2265
- replies follow MongoDB 8.0, whose document size tiers are larger than 7.0's;
2266
- where NoSQLDB still differs from MongoDB 8.0, the list at the end of this
2267
- section says so.
2268
-
2269
- OP_MSG parsing rejects unknown required flags, invalid CRC-32C checksums,
2270
- duplicate BSON keys, noncanonical array indexes, malformed section boundaries,
2271
- multiple body sections, duplicate or colliding document-sequence identifiers,
2272
- and missing or non-string `$db` fields. Early command-envelope validation then
2273
- accepts document sequences only as `insert.documents`, `update.updates`, or
2274
- `delete.deletes`, before authentication or command resources are acquired. A
2275
- write command refuses any other sequence as an unknown field of its own,
2276
- `IDLUnknownField` (40415) naming `insert.x`, or `insert` for an empty
2277
- identifier, as MongoDB 8 does; any other command refuses document sequences with
2278
- `InvalidOptions` (72). All three accepted forms are consumed as the
2279
- corresponding bulk command argument. Unknown optional flags are ignored.
2280
-
2281
- As in MongoDB 8, a complete frame that is malformed or over a document limit is
2282
- answered with an error reply to its request id, and the connection stays usable
2283
- for the requests behind it: for example `InvalidBSON` (22) for a bad document
2284
- length or a body cut short, `Overflow` (15) for nesting beyond 100 levels, the
2285
- `Location404xx` codes for malformed sections, `Location51252` for a checksum
2286
- that leaves no room for a section, and `BSONObjectTooLarge` (10334) for an
2287
- oversized document. The 10334 message shows the document's first element as
2288
- MongoDB's does, cut after 160 bytes. In MongoDB 8's order, a document's
2289
- structure (lengths, terminators, element types and booleans) and nesting are
2290
- checked before its size, and a message's sections and BSON before its checksum.
2291
- A defect in a legacy OP_QUERY document, its nesting included, is `InvalidBSON`
2292
- (22), as MongoDB 8 answers it, and bytes after an OP_QUERY's field selector are
2293
- ignored, as MongoDB 8 ignores them. A refused `moreToCome` request gets no
2294
- reply. A frame that cannot be delimited or trusted closes the connection, again
2295
- as in MongoDB: an invalid length prefix, an OP_MSG too short to hold its flags,
2296
- a request whose `responseTo` is not 0, an unknown required flag, more than 128
2297
- sections, or a checksum mismatch.
2298
-
2299
- A message is limited to 48,000,000 bytes. A command body may be up to 16,809,984
2300
- bytes and a document-sequence or legacy OP_QUERY document up to 16,793,600 bytes,
2301
- MongoDB 8's 16 MiB plus 32 KiB and 16 KiB of command overhead; a stored document
2302
- is limited to 16,777,216 bytes, and an insert row over it is that row's
2303
- `BSONObjectTooLarge` write error. An `insert`, `update` or `delete` batch must
2304
- hold 1 to 10,000 operations, the advertised `maxWriteBatchSize`; other batches
2305
- are refused with `InvalidLength` (16), and drivers split larger input batches.
2306
-
2307
- Before any of a request is decoded, its BSON is checked and the heap its decoded
2308
- form takes is computed from the bytes alone, each allocation rounded up as the
2309
- allocator rounds it. A request holds that charge against the server's
2310
- decoded-memory budget, 1 GiB by default, until its command has finished with
2311
- it. A request whose decoded form takes 16 KiB or less, as handshakes, pings
2312
- and ordinary commands do, holds it against a separate 16 MiB share kept beside
2313
- the budget instead, which holds one such request for each of the 1,024
2314
- connections a server accepts, so it never waits behind larger requests. A
2315
- request whose frame is small but whose decoded form is larger, such as a find
2316
- with a long `$in` list, waits in the budget with the large ones. A request
2317
- that needs more than the
2318
- whole budget is refused whole with `BSONObjectTooLarge` (10334) and the message
2319
- "send smaller batches", after its sections and BSON are checked and before its
2320
- checksum. The number of elements, nested documents and arrays a request holds
2321
- is not limited, so a full batch of 10,000 wide documents is accepted as in
2322
- MongoDB. A request that does not fit what the budget has left waits for it
2323
- behind every request of its share that arrived before it, for at most its
2324
- `maxTimeMS` or five seconds from when it arrived. A request still waiting then
2325
- is answered without being decoded or run: with `MaxTimeMSExpired` (50) when its
2326
- `maxTimeMS` is five seconds or less, and otherwise with `LockTimeout` (24),
2327
- which a transaction statement gets with the `TransientTransactionError` label.
2328
- A statement of a running transaction answered so aborts its transaction; a
2329
- `commitTransaction` or `abortTransaction` answered so, or a statement that would
2330
- start its transaction, leaves the transaction as it was. A `moreToCome` request
2331
- still waiting closes its connection. A command's `maxTimeMS` counts from when
2332
- the server had the whole request, so the wait is part of it; a request sent
2333
- while the connection's previous command ran counts from when that command
2334
- finished, as MongoDB reads it only then. `maxTimeMS: 0` means no limit, as in
2335
- MongoDB, so such a request waits the five seconds and gets `LockTimeout`. A
2336
- legacy OP_QUERY command wrapped in `$query` is answered from an envelope that
2337
- misses its `maxTimeMS` and `lsid`, so it waits five seconds and gets
2338
- `LockTimeout` without a transaction label.
2339
-
2340
2077
  The `moreToCome` request flag is accepted only for the exact `endSessions` or
2341
2078
  `killSessions` cleanup envelope: an `admin` command body containing only the
2342
2079
  session array, `writeConcern: { w: 0 }`, and `$db: "admin"`, with no document
@@ -2402,40 +2139,10 @@ The decoded budget also bounds the largest request the server accepts, so a
2402
2139
  smaller one refuses smaller requests with `BSONObjectTooLarge`. Each server has
2403
2140
  its own budgets, so servers in one process do not share them.
2404
2141
 
2405
- NoSQLDB still differs from MongoDB 8.0 on the wire in these points:
2406
-
2407
- - An opcode other than OP_MSG and OP_QUERY closes the connection. MongoDB 8
2408
- ignores most unknown opcodes and answers the legacy `OP_GET_MORE` with error
2409
- 5739101.
2410
- - A legacy OP_QUERY to a collection other than `$cmd` is routed as a `find`,
2411
- where MongoDB 8 answers 5739101.
2412
- - Nesting is limited to 100 levels; MongoDB 8 allows 200.
2413
- - A write batch over 10,000 operations is refused with `InvalidLength` (16);
2414
- MongoDB 8 accepts up to 100,000.
2415
- - A request whose decoded form needs more than the decoded-memory budget, 1 GiB
2416
- by default, is refused with `BSONObjectTooLarge` (10334); MongoDB 8 has no
2417
- such limit.
2418
- - A request that waits for decode memory longer than its `maxTimeMS` or five
2419
- seconds is answered with `MaxTimeMSExpired` (50) or `LockTimeout` (24)
2420
- without being run; MongoDB 8 has no decode budget.
2421
2142
  - A frame over 8 KiB whose bytes average less than 1 MiB per second after a
2422
2143
  10-second grace closes its connection while another connection waits for
2423
- the encoded budget or more than seven eighths of it are charged; MongoDB 8
2144
+ the encoded budget or more than seven eighths of it are charged; MongoDB
2424
2145
  has no such limit.
2425
- - The identifier a `Location40431` or `Location40433` message names is cut
2426
- after 256 bytes; MongoDB 8 names it whole.
2427
- - A duplicate key anywhere in a request is refused with `InvalidBSON` (22);
2428
- MongoDB 8 does not refuse duplicate keys on the wire.
2429
- - A field name or string that is not valid UTF-8 is refused with `InvalidBSON`
2430
- (22); MongoDB 8 does not check UTF-8 on the wire and stores such data.
2431
- - Most `InvalidBSON` (22) messages describe the defect in NoSQLDB's own words.
2432
- - A message with three document sequences gets an error reply, where MongoDB 8
2433
- closes the connection.
2434
- - A command other than `insert`, `update` and `delete` refuses document
2435
- sequences with `InvalidOptions` (72); MongoDB 8 runs it.
2436
- - `moreToCome` is accepted only for the session cleanup envelope above, where
2437
- MongoDB 8 runs any command sent with it.
2438
- - A request that carries a checksum is answered without one.
2439
2146
 
2440
2147
  ### Identifying the Engine
2441
2148
 
@@ -2458,9 +2165,9 @@ is. Every `hello`, `ismaster` and `isMaster` reply carries a self-contained
2458
2165
  `version` is the released `@lossless.org/nosqldb` version the running binary was
2459
2166
  built from: the Rust build compiles it in from the package manifest, so the
2460
2167
  answer belongs to the shipped artifact and cannot drift from it.
2461
- `mongodbCompatibility` is the MongoDB release whose wire protocol and command
2462
- semantics this engine implements — a compatibility statement, never a claim to
2463
- be that build.
2168
+ `mongodbCompatibility` is the compatibility version NoSQLDB reports to drivers
2169
+ and tools for the command surface it implements to be MongoDB compatible — a
2170
+ compatibility value, never a claim to be a MongoDB build.
2464
2171
 
2465
2172
  The handshake carries the identity unconditionally, before authentication, so a
2466
2173
  client can derive its backend from the connection it already has. `buildInfo` is
@@ -2468,7 +2175,7 @@ available to **every authenticated connection**, including a `readWrite`-only
2468
2175
  tenant user and an allocation read grant; it requires no `clusterMonitor` role
2469
2176
  and creates no session state. `buildInfo` answers from build metadata alone and
2470
2177
  therefore still answers on a fenced or closed runtime, while `hello` fails
2471
- closed. It answers in MongoDB's shape so existing tooling keeps working:
2178
+ closed. It answers in a MongoDB-compatible shape so existing tooling keeps working:
2472
2179
 
2473
2180
  ```json
2474
2181
  {
@@ -2494,16 +2201,65 @@ reported, including `version` and `process: "rustdb"`, and now reports the same
2494
2201
  `nosqldb` identity beside them — but it remains a `clusterMonitor` command, so
2495
2202
  least-privilege connections use `hello` or `buildInfo`.
2496
2203
 
2204
+ `serverStatus.engineFigures` reports the storage engine's own figures, each
2205
+ read from a counter or from the allocator's own statistics, so forming them
2206
+ takes no time that grows with the data. Every count is an int64 that is 0 when
2207
+ the server opens its storage and never decreases while it runs. `commitMode` is
2208
+ `buffered`: commits go through the write buffer, the only commit path.
2209
+ `bufferedBytes` is the bytes of committed writes that the buffer holds and no
2210
+ merge has yet written into the tree, the sum of `activeGenerationBytes` and
2211
+ `frozenGenerationBytes`; `commits` counts the publications made durable and
2212
+ visible, and `mergesStarted` and `mergesCompleted` the buffer's merges.
2213
+ `freezes` counts the buffer's generations frozen for a merge, by cause: `size`
2214
+ when the generation reached its size, `idle` when no commit arrived for the
2215
+ quiet period, `age` when its oldest commit reached the age limit, and `prune`
2216
+ when records that history pruning wrote waited for a merge. `backstopStall`
2217
+ counts the commits that waited at the buffer's backstop (`stalls`) and the
2218
+ nanoseconds they waited (`stalledNanoseconds`), and gives the commits waiting
2219
+ there now (`stalledWriters`). A commit waits there when it would take the
2220
+ active generation past its size while a generation is pending, from its
2221
+ freeze until the checkpoint after its merge is durable, or while commits that
2222
+ passed the backstop before it and are not linked yet fill the active
2223
+ generation; and a background commit waits there at its claim on the buffer
2224
+ while another background commit holds one. `refusals` counts the
2225
+ requests for space that the engine refused for lack of free
2226
+ space, a commit's or the engine's own work's (`space`), and the
2227
+ commits refused after waiting at the backstop as long as their limit
2228
+ allows (`backstop`). While a merge cannot get room,
2229
+ `serverStatus.engineFigures.mergeWedge` reports what it lacks (`bytesShort` or
2230
+ `segmentsShort`), and is null again once a later attempt of the merge is
2231
+ admitted; `mergeRefusals` counts the merge attempts refused since the root
2232
+ opened. `restarts` counts the write preparations started again by cause,
2233
+ `catalogChange`, `pruningWindow` and `merge`; one for a conflict on a document
2234
+ or unique key counts under none. `bytesWritten` counts the bytes handed to the
2235
+ file system for the storage files by duty: `redo`, `merge`, `cleaner`,
2236
+ `indexes`, `checkpoint`, `directory` and `fill`. `liveBytes` is the bytes the
2237
+ current state reaches and `fixedBytes` the bytes the engine's files hold apart
2238
+ from data: the anchor, segment headers and pre-sized log space.
2239
+ `directoryBytes` and `cacheResidentBytes` are the memory that the directory of
2240
+ stored frames and the node cache hold, and `cacheBudgetBytes` is the cache
2241
+ budget that `storageEngine.cacheBytes` reports.
2242
+ `serverStatus.engineFigures.pinnedBytes` is the bytes in the storage's data
2243
+ segments that the engine keeps, where it would otherwise free them, only
2244
+ because a live snapshot, such as an open cursor's, can still read superseded
2245
+ data in them: the value its latest maintenance round found, and 0 when no
2246
+ snapshot keeps any. `allocator` states whether a
2247
+ check at run time found the shipped allocator (`verified`), and only then its
2248
+ arena count, its decay times in milliseconds, whether it runs background
2249
+ threads, and the bytes allocated through it and resident; while `verified` is
2250
+ false, those fields are null.
2251
+
2497
2252
  ### Read and Write Concerns and Wire Deadlines
2498
2253
 
2499
2254
  NoSQLDB serves every database from exactly one voting node and states its
2500
2255
  concern profile in those terms instead of ignoring or blanket-rejecting the
2501
2256
  fields:
2502
2257
 
2503
- - **Write concern.** Every acknowledged write is fsynced to the write-ahead log
2504
- and the data file before the response is sent, so `w: 1`, `w: "majority"`
2258
+ - **Write concern.** Every acknowledged write is in the write-ahead log before
2259
+ the response is sent: fsynced there with file storage, while memory storage
2260
+ keeps its log in the process's memory. So `w: 1`, `w: "majority"`
2505
2261
  (the majority of one voter is that voter), and `j: true` are all satisfied by
2506
- the same durable commit. `w: 0` is acknowledged the same way whenever a
2262
+ the same commit. `w: 0` is acknowledged the same way whenever a
2507
2263
  response is expected; the exact `writeConcern: { w: 0 }` `endSessions` and
2508
2264
  `killSessions` envelopes still run without a transport response. `wtimeout`
2509
2265
  bounds a replication wait that does not exist on one node and has no
@@ -2523,65 +2279,6 @@ fields:
2523
2279
  and `aggregate` only, and `afterClusterTime`, `atClusterTime`, and
2524
2280
  `afterOpTime` are rejected with `InvalidOptions` because a single node issues
2525
2281
  no cluster time.
2526
- - **Deadlines.** `maxTimeMS` attaches a cooperative deadline to the command,
2527
- counted from when the server had the whole request, or from when the
2528
- connection's previous command finished for a request sent while it ran. It
2529
- is checked while the request waits for decode memory, at admission, during
2530
- namespace-lock and maintenance-gate waits, before `find`, `count`, and
2531
- `distinct` materialize documents, between
2532
- documents in `insert` and `delete` batches, and immediately before a
2533
- transaction commit is handed to storage; expiry fails with `MaxTimeMSExpired`
2534
- (code 50). A single storage step or an in-progress `update` batch runs to completion
2535
- before the next check, so a write that expires after its first documents
2536
- were published reports the expiry while those documents remain durable,
2537
- exactly like an interrupted MongoDB batch. The official driver transmits a
2538
- transaction's `maxCommitTimeMS` as `maxTimeMS` on `commitTransaction`; a
2539
- commit that expires before its batch is handed to storage publishes nothing,
2540
- records the transaction as aborted, and carries the
2541
- `UnknownTransactionCommitResult` label; the driver's commit retry then sees
2542
- `NoSuchTransaction` with `TransientTransactionError` and restarts the whole
2543
- transaction. `getMore` rejects `maxTimeMS` with `BadValue` because NoSQLDB
2544
- has no awaitData cursors, and a literal `maxCommitTimeMS` field is rejected
2545
- with `InvalidOptions`.
2546
- - **Database waits.** DDL that drops or reshapes collections (`drop`,
2547
- `dropIndexes`, `collMod`, `dropDatabase`, `renameCollection`,
2548
- `$out`/`$merge`) holds its database exclusively; other commands on that
2549
- database wait for it, as they wait for a MongoDB database lock. Creating a
2550
- collection or its indexes (`create`, `createIndexes`) holds only that
2551
- collection exclusively, as MongoDB locks the collection: it waits for the
2552
- open transactions whose statements targeted that collection (a collection
2553
- a transaction reads only through `$lookup` or `$unionWith` is not held; the
2554
- transaction reads its snapshot of it), transaction statements on that
2555
- collection that arrive after it queue behind it, and everything else
2556
- proceeds. Commands outside a transaction take no collection
2557
- gate; writes still queue for the storage writer, which an index build holds
2558
- until its index is ready. A command outside a
2559
- transaction waits first in, first out for as long as it may run: until
2560
- `maxTimeMS` (`MaxTimeMSExpired`, code 50), until its client disconnects or
2561
- shuts down its sending side (the wait ends with `ClientDisconnect`, code
2562
- 279, which a client that still reads receives before the connection closes)
2563
- or until the server shuts down (`InterruptedAtShutdown`, code 11600). A
2564
- database DDL command likewise waits for the open transactions on its
2565
- database to commit or abort, and every command on that database that
2566
- arrives after it queues behind it, including reads of other collections. An
2567
- open transaction therefore delays DDL and the commands behind it at most
2568
- until the transaction lifetime limit (60 seconds, MongoDB's
2569
- `transactionLifetimeLimitSeconds`) aborts it.
2570
- A transaction statement that needs a further database or collection keeps a
2571
- fixed five-second limit and then fails with `LockTimeout` (code 24, labelled
2572
- `TransientTransactionError`), so a transaction never waits without bound while it
2573
- holds its databases and collections; like any failed statement, it aborts
2574
- the transaction and releases them. One transaction targets at most 4,096
2575
- distinct collections across all its databases; statements that revisit a
2576
- collection retain no extra gate. This matches the process-wide collection-lock
2577
- registry's 4,096-live-identity bound; concurrent transactions and collection
2578
- DDL share that registry. A command that finds the shared registry full fails
2579
- with `LockTimeout` (code 24; labelled `TransientTransactionError` in a
2580
- transaction) and can be retried once in-flight operations complete. It bounds process-local locks, not document staging,
2581
- WAL record size or atomic publication. A statement targeting one more fails
2582
- with code 16500 (`Location16500`, MongoDB 8's code name for it) and aborts it. The service operations (export,
2583
- import, tenant and publication fencing) keep the five-second gate-wait limit and report
2584
- `EFENCE_BUSY`.
2585
2282
 
2586
2283
  Callers may therefore rely on a successful NoSQLDB response as a durable,
2587
2284
  single-node acknowledgement. They must not read it as a replica-backed
@@ -2615,7 +2312,7 @@ The Rust engine is organized as a Cargo workspace with 12 focused crates:
2615
2312
  | `rustdb-wire` | Wire protocol parser/encoder (OP_MSG, OP_QUERY, OP_REPLY) |
2616
2313
  | `rustdb-query` | Query matcher, update engine, aggregation, sort, projection |
2617
2314
  | `rustdb-state` | Internal deterministic storage commands, stable retry identities, and durable receipts |
2618
- | `rustdb-kernel` | Native paged committed-log kernel with stable timelines, contiguous LSNs, strict recovery and exact receipt replay |
2315
+ | `rustdb-kernel` | Native storage format 3: a log of commits, a write buffer merged into a tree, checkpoints and a cleaner, with stable timelines, contiguous LSNs and strict recovery; and the format-2 reader of 10.x roots |
2619
2316
  | `rustdb-replication` | Internal non-serving OpenRaft 0.9.25 one-voter fresh-root foundation; not linked into the released `rustdb` server |
2620
2317
  | `rustdb-storage` | Native catalogs, documents, exact counts, staged imports, reclamation and bounded OpLog |
2621
2318
  | `rustdb-index` | Native index records, query plans, uniqueness, bounded builds and TTL |
@@ -2626,57 +2323,35 @@ The Rust engine is organized as a Cargo workspace with 12 focused crates:
2626
2323
 
2627
2324
  The native MVCC engine serves both memory and file storage.
2628
2325
  Its Rust APIs use `mvcc` modules and `Mvcc` type names independently of npm
2629
- versions and persisted format versions. The K2 kernel stores immutable paged
2630
- roots, typed BSON records and checksummed commit markers in one append stream.
2631
- Snapshots pin roots; normal writes append changed pages. Descriptor-relative
2326
+ versions and persisted format versions. The format-3 store writes each
2327
+ publication as one checksummed `COMMIT` frame of typed BSON records to its
2328
+ log, keeps recent writes in an in-memory buffer, merges them into a
2329
+ copy-on-write tree in data segments and writes checkpoints from which
2330
+ recovery replays the log. Snapshots pin a published view, and maintenance
2331
+ frees nothing a pinned snapshot can reach. Descriptor-relative
2632
2332
  access and the enclosing native file-root lease protect the entire lifecycle.
2633
2333
  Ambiguous publication fences the owner until recovery. Confirmed close drains
2634
2334
  requests, sessions, cursors and retained workers before releasing ownership.
2635
2335
 
2636
- The bounded page cache retains validated key prefixes for point lookups when
2637
- large separators cannot fit as complete nodes. Prefix coverage grows with the
2638
- probe length and preserves exact ordering; scans and writes use complete keys.
2639
- This keeps unrelated small-key operations from repeatedly loading wide index
2640
- separators without increasing the cache budget or changing the stored format.
2641
- A collection scan looks up the backpointers and documents of each page's rows
2642
- in one ordered pass over the tree and reads their values in file order. Scans
2643
- and range pages read adjacent values together, up to 64 KiB per system call,
2644
- and still verify each value on its own.
2645
-
2646
- Verified record values have their own bounded cache, 4 MiB by default, so value
2647
- churn never evicts tree pages. A point read admits a value of up to 64 KiB, or
2648
- an eighth of the cache if that is smaller. A range read admits like a point
2649
- read only when it returns a single record and exhausts its range, such as an
2650
- exact unique-index probe; other range reads admit nothing. A query or
2651
- transaction read whose whole index selection is one entry caches its document
2652
- like a point find. The other per-record reads of a selection or pass use
2653
- cached values but admit none: the documents of collection and index scans, the
2654
- identities of covered `_id` scans, the index entries a multi-value `$in`
2655
- probes, and the reads of validation, stopped
2656
- inspection, TTL and authentication passes, including their single-record
2657
- probes. A user listing and a write's own probes of unique owners, index
2658
- entries and natural-order records admit nothing either. A scan, maintenance
2659
- pass or listing therefore cannot flush hot catalog records and documents.
2660
- A commit forgets the cached value of every record it replaces or deletes, so
2661
- a write, however many documents it changes, leaves behind at most what its
2662
- query would have cached as a find. Frames are immutable and append-only, and a
2663
- snapshot reaches only the frames its own tree references: an older snapshot
2664
- that still needs a forgotten value reads it from storage again, and a new
2665
- version is cached once a read looks it up. As with tree pages, a value is
2666
- verified once when it is cached;
2667
- storage corruption of a cached frame is detected after eviction or generation
2668
- retirement.
2669
-
2670
- Both caches evict by CLOCK. Each cached node and value carries a referenced
2671
- bit. Every read sets it on the inner tree nodes it passes through, since each
2672
- lookup beneath such a node passes through it again. On leaves and values, point
2673
- lookups, the range reads of point queries and value admission set it; scans,
2674
- maintenance passes, generation copies and a commit's own reads leave it as they
2675
- find it, and the leaves and values they cache start without it. To make room, a
2676
- hand sweeps the entries in turn, clears each set bit it passes and evicts the
2677
- first entry whose bit is clear. A leaf or value that point reads use therefore
2678
- outlasts the scan entries cached after it by one turn of the hand, and a cache
2679
- hit costs no reordering.
2336
+ The store keeps verified tree nodes in one node cache, half of its memory
2337
+ budget, evicted by CLOCK. The budget is `storageBudgetBytes` where that option
2338
+ of `NoSqlDbServer` is set, from 4 MiB to 1 TiB, and otherwise a sixteenth of
2339
+ the memory the process may use, measured as for the inbound budgets above, from
2340
+ 16 MiB to 4 GiB; `serverStatus.storageEngine.cacheBytes` reports the cache's
2341
+ half. A root the server serves takes it when it is created or opened and keeps it
2342
+ until it closes; the stopped operations' reads of a stopped root, a start's
2343
+ checks of a root before it serves it and a 10.x conversion open the store
2344
+ with 64 MiB. Each cached node carries a referenced bit:
2345
+ inner nodes enter with it set and leaves without it, and a point read sets it
2346
+ on the nodes it uses. To make room, a hand sweeps the entries in turn, clears
2347
+ each set bit it passes and evicts the first entry whose bit is clear. A read
2348
+ that is one row of a pass over many records, such as a scan, a merge or a page
2349
+ of the commit journal, clears no bit and is cached only in place of an
2350
+ unreferenced entry at the hand, so such reads cannot displace the nodes point
2351
+ reads keep. A publication removes the nodes it supersedes from the cache.
2352
+ There is no value cache: a value stored outside its leaf is read from storage
2353
+ each time. A collection scan reads each page of documents as one range read of
2354
+ their bodies in natural order, with no lookup per document.
2680
2355
 
2681
2356
  Database and collection lookups, absent names included, are remembered for the
2682
2357
  newest committed root a read has used, up to 256 names and 1 MiB, so the
@@ -2690,7 +2365,7 @@ one root list its indexes once. The first listing on a root reads the catalog
2690
2365
  whole; a catalog of more than 64 indexes, aliases included, and a listing that
2691
2366
  fails are not kept.
2692
2367
 
2693
- Document, catalog, exact-count, natural-order, index-entry, unique-owner,
2368
+ Document, catalog, statistics, natural-order, index-entry, unique-owner,
2694
2369
  authentication, fencing and retry-receipt records share atomic publications.
2695
2370
  Transactions retain changed documents and immutable read views with byte and
2696
2371
  concurrency admission; they do not copy a complete database snapshot or impose a
@@ -2701,15 +2376,25 @@ generated IDs and zero-match outcomes, across restart. Failed statements do not
2701
2376
  receive success receipts. Session termination drains active work and retires its
2702
2377
  cursors.
2703
2378
 
2704
- Custom indexes build in bounded batches under a writer reservation and become
2705
- Ready together. Startup resumes interrupted builds under native ownership before
2706
- serving. If a resumed build confirms duplicate keys or exceeds its work budget,
2707
- its Building state remains intact and a secret-free warning identifies the need
2708
- for explicit index repair. Reads avoid that index; indexed writes fail closed.
2709
- Use the supported index drop/recreate operation to resolve it. Cancellation,
2710
- corruption and uncertain publication fail startup. Held databases remain closed
2711
- and are skipped during recovery; releasing a hold does not run extra mutations.
2712
- Their interrupted indexes can recover on a later restart.
2379
+ Custom indexes build in the background: `createIndexes` publishes them as
2380
+ building in one short commit and answers once they are ready, while other writes
2381
+ go on. A build sorts its entries off the writer, after reserving the space its
2382
+ sort runs and its output need, with its runs in a private scratch directory
2383
+ inside a file root and in memory on a memory root, and attaches the entries in
2384
+ one commit; it then applies the writes made meanwhile until the writers keep
2385
+ its indexes up to date in their own commits, and makes its indexes ready
2386
+ together in a commit that carries no entry. No read uses its indexes
2387
+ before then. A request for the same indexes joins a running build, and
2388
+ `serverStatus` reports builds under `indexBuilds`. A build that its collection's
2389
+ writes outpace, or that meets a resource limit, fails with an explicit refusal
2390
+ and leaves no index; a duplicate key fails a unique build with `DuplicateKey`
2391
+ (11000). Interrupted builds restart from the beginning under native ownership
2392
+ before serving, and run in the background, so serving does not wait for them.
2393
+ A restarted build that fails removes its indexes, restores a quarantined index
2394
+ it was rebuilding, logs a warning that names neither the index nor a key, and
2395
+ shows as failed in `indexBuilds` until the next build of that index; it is not
2396
+ retried by itself. Held databases remain closed and are skipped during
2397
+ recovery; releasing the hold restarts their interrupted builds.
2713
2398
 
2714
2399
  Index names and aliases, sparse and unique constraints, empty collections,
2715
2400
  restricted-view catalogs and exact BSON survive supported database export/import.
@@ -2728,7 +2413,7 @@ The serving profile is:
2728
2413
  | Auth/sessions | SCRAM-SHA-256, logical sessions, snapshot transactions, commit/abort and durable retryable writes |
2729
2414
  | Management | Health, tenants, grants, attestation, fencing, publication holds, export/import and content digest |
2730
2415
  | Debug | Bounded current-session OpLog, atomic data revert, document/collection pages and dashboard |
2731
- | Maintenance | Bounded TTL expiry, retired record reclamation, orphan import retirement and generation compaction |
2416
+ | Maintenance | Bounded TTL expiry, retired record reclamation, orphan import retirement, and the store's merges, checkpoints, tombstone pruning and cleaning |
2732
2417
  | Stopped storage | Native inspection and relocation; isolated authenticated rehearsal on its qualified platform profile |
2733
2418
  | Import/control admission | 100 MiB encoded import; bounded private staging and a small final catalog/receipt commit. Other control requests: 1 MiB |
2734
2419
  | External cluster | Replication and HA remain unsupported; the one-voter OpenRaft foundation is not linked into serving |
@@ -2737,16 +2422,20 @@ These are operation and resource budgets, not total database/document limits.
2737
2422
  Unsupported options and commands reject explicitly. Imports that cannot fit one
2738
2423
  derived document or index entry within the native commit budget reject before
2739
2424
  publishing the replacement. Reclamation preserves pinned roots and retires parent
2740
- identities only after their children have been handled. Automatic compaction
2741
- requires at least 64 MiB of growth and 50% growth over its prior baseline.
2425
+ identities only after their children have been handled. The store's cleaner
2426
+ moves the live data out of a data segment of which at least a third of the
2427
+ written bytes is garbage, and a segment is removed once no checkpoint or
2428
+ pinned snapshot needs it.
2742
2429
 
2743
- There is one serving engine and no Bitcask fallback. No historical storage or
2744
- authentication conversion runs during startup or rehearsal. Conversion code,
2745
- when explicitly commissioned, belongs exclusively in top-level `ts_migration/`.
2430
+ There is one serving engine and no Bitcask fallback. A start converts a root that
2431
+ a 10.x release wrote into format 3 (see **Storage format migration** under File
2432
+ startup admission); no other storage conversion and no authentication conversion
2433
+ runs during startup or rehearsal. Any other conversion code, when explicitly
2434
+ commissioned, belongs exclusively in top-level `ts_migration/`.
2746
2435
 
2747
2436
  NoSQLDB makes no general MongoDB performance or feature-parity claim.
2748
2437
  Versioned packages contain Linux amd64/arm64 and macOS amd64/arm64 native
2749
- executables through [@git.zone/tsrust](https://www.npmjs.com/package/@git.zone/tsrust).
2438
+ executables through [@git.zone/rsrepo](https://www.npmjs.com/package/@git.zone/rsrepo).
2750
2439
 
2751
2440
  ### Multi-host release artifacts
2752
2441
 
@@ -2768,7 +2457,7 @@ The native distribution terms and qualified library requirements are recorded in
2768
2457
  extract and install these files independently of its compiled JavaScript bundle;
2769
2458
  NoSQLDB does not download engines at startup.
2770
2459
 
2771
- 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.
2460
+ A plain `rsrepo` 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); `rsrepo --configured-targets` builds the host's complete partition from `targetsByHost`. The development suite checks the packaged engines of the host architecture. Releases use `rsrepo 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.
2772
2461
 
2773
2462
  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:
2774
2463
 
@@ -2777,11 +2466,11 @@ export NOSQLDB_RELEASE_MACOS_SSH="release-user@mac-builder.example"
2777
2466
  export NOSQLDB_RELEASE_MACOS_TEMP_ROOT="/absolute/dedicated/nosqldb-release"
2778
2467
  ```
2779
2468
 
2780
- `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:
2469
+ `gitzone release` first runs `pnpm run release:check-builders`, before creating a version commit or tag. It verifies the project-local rsrepo version and rejects missing cross-toolchains or emulation, an unsuitable macOS host, missing tools, or an unsafe remote temporary root. The release build then:
2781
2470
 
2782
2471
  1. Builds the TypeScript package and debug UI bundle from the version commit.
2783
2472
  2. Transfers an exact Git bundle into isolated local Linux and SSH macOS worker workspaces and installs each frozen lockfile.
2784
- 3. Runs the configured `release:verify-native` command on each worker: the full Rust workspace, TypeScript test checks, a `tsrust --configured-targets` build 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/tmp` on Linux and canonical `/tmp` on macOS. Linux builders must provide `/var/tmp` on a native-supported durable filesystem; tmpfs is rejected. Failures retain diagnostics and prevent assembly.
2473
+ 3. Runs the configured `release:verify-native` command on each worker: the full Rust workspace, TypeScript test checks, a `rsrepo --configured-targets` build 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/tmp` on Linux and canonical `/tmp` on macOS. Linux builders must provide `/var/tmp` on a native-supported durable filesystem; tmpfs is rejected. Failures retain diagnostics and prevent assembly.
2785
2474
  4. Builds and executes GNU and musl Linux amd64/arm64 plus macOS arm64/amd64 binaries on their assigned workers.
2786
2475
  5. Retrieves bounded artifact sets and transactionally publishes the complete provenance-checked matrix into `dist_rust/`.
2787
2476
 
@@ -2793,13 +2482,38 @@ No host address or path is stored in the repository. A failed run retains its un
2793
2482
 
2794
2483
  - Atomic native publications include data, indexes, authentication, fences and
2795
2484
  durable outcomes. File writes are acknowledged after durable commit.
2796
- - Immutable roots keep reads stable through concurrent writes and compaction.
2485
+ - Snapshots keep reads stable through concurrent writes and maintenance:
2486
+ merges, checkpoints and the cleaner free nothing a snapshot can reach.
2797
2487
  - Checksums, frame identities and contiguous positions validate committed
2798
2488
  history. Only a valid incomplete final append is repairable during owned
2799
2489
  startup; complete or interior corruption is rejected.
2490
+ - A device that loses acknowledged bytes at the end of the log and reads
2491
+ them back as zeros cannot be detected: recovery takes the zeros for the end
2492
+ of the log. The loss reaches back at most to the newest durable
2493
+ checkpoint's redo start. The commits it can lose hold at most half the
2494
+ buffer budget plus two holds, counted as their keys and values plus 64
2495
+ bytes per record: one hold of commits, which is one commit or one group
2496
+ or batch of commits, and one background commit's claim on the buffer.
2497
+ The server sets the budget when it opens a root, to
2498
+ `storageBudgetBytes` where that option is set (4 MiB to 1 TiB), and
2499
+ otherwise to a sixteenth of the memory the process may use, from 16 MiB to
2500
+ 4 GiB: 64 MiB plus the two holds can be lost in a 2 GiB container, and at
2501
+ most 2 GiB plus the two holds unless the option sets a larger budget.
2502
+ Each hold is at most the batch limit, 64 MiB by default.
2800
2503
  - Uncertain publication fences the engine until confirmed recovery.
2801
- - Bounded maintenance reclaims unreachable records and obsolete generations
2802
- after their readers release them.
2504
+ - Bounded maintenance reclaims unreachable records, and segments that hold
2505
+ only superseded data, after their readers release them.
2506
+ - Disk use under live snapshots is unbounded: the store does not limit how
2507
+ long a snapshot stays open. A segment whose data has all been superseded
2508
+ stays on disk while a snapshot that can still read it is open, and
2509
+ `serverStatus` reports those bytes at
2510
+ `serverStatus.engineFigures.pinnedBytes`.
2511
+ - Writes are admitted at full speed until the free space falls to the
2512
+ store's floor: 64 MiB or two segments, whichever is more, beyond what the
2513
+ write buffer holds. There, every write but a delete or a drop is refused
2514
+ with the capacity code 16500 and writes nothing. Deletes and drops are
2515
+ still admitted, down to two segments, while no snapshot older than the
2516
+ newest write is open.
2803
2517
  - A durable session whose records this version cannot read does not stop
2804
2518
  maintenance. The obsolete-outcome sweep passes over it and leaves its records
2805
2519
  in place. It logs an error at most once a minute and counts the session in
@@ -2823,22 +2537,48 @@ deadline, 100,000 filesystem-entry and 1 TiB byte limits, and bounded native
2823
2537
  record/result admission. This is storage integrity evidence; it does not replace
2824
2538
  the full authenticated rehearsal or production ownership protocol.
2825
2539
 
2540
+ A failure names a stable code (`NoSQLDB stopped inspection failed with code …`).
2541
+ Two codes describe states the tool deliberately does not validate:
2542
+
2543
+ - `RECOVERY_REQUIRED` while a stopped relocation or alignment has left its
2544
+ workspace in the root: finish that relocation or alignment, or abort the
2545
+ alignment, with its exact request, then validate again. A root of this
2546
+ release that a crash left needs no start first: the tool reads it as a start
2547
+ would recover it, writing nothing. A root that a 10.x process left without a
2548
+ clean stop also reports `RECOVERY_REQUIRED`; a start of this release converts
2549
+ it as of its last completed commit.
2550
+ - `UNSUPPORTED` while an index build is unfinished: an index that is building,
2551
+ quarantined, or importing into a collection that is not dropped. A start
2552
+ restarts interrupted builds from the beginning in the background, without
2553
+ waiting for them before it serves, so a root stopped before they publish
2554
+ still reports `UNSUPPORTED`, as does one whose database's publication hold
2555
+ keeps them back. A restarted build that fails, for example on duplicate
2556
+ keys, removes its indexes or restores the quarantined index it was
2557
+ rebuilding, which validation reports until that index is dropped or rebuilt.
2558
+
2826
2559
  ---
2827
2560
 
2828
2561
  ## Testing Example
2829
2562
 
2830
- The offline regression `test/test.commit-journal-compatibility.node.ts` creates
2831
- disposable roots with the exact published 10.8.1 executable, then qualifies the
2832
- current engine against them. A normal `pnpm install --frozen-lockfile` provides
2833
- the exact `@lossless.org/nosqldb@10.8.1` package through the pinned development
2834
- alias `@lossless.org/nosqldb-baseline-1081`, with registry integrity in the lockfile.
2835
- The test reads its platform executable directly from `node_modules`, with no
2836
- manual artifact cache or environment variable. It also pins each supported
2837
- executable's published SHA-256
2838
- and fails on missing or substituted artifacts; it never fetches, skips, builds
2839
- the baseline, or changes dependency manifests at runtime. Build the current
2840
- engine normally before running the test. This qualifies opening existing data
2841
- without rewriting its native format, followed by durable journal publications.
2563
+ The offline regression `test/test.commit-journal-compatibility.node.ts` reads
2564
+ `test/format-2/compatibility.tar`, a root that the published 10.8.1 release
2565
+ wrote once from the definition in `test/format-2/roots.ts`
2566
+ (`test/format-2/archives.md` records how). It checks every archive in
2567
+ `test/format-2/` against `SHA256SUMS`, and each of its tests that reads the root extracts that
2568
+ archive into a new directory that only its owner can access, so the root it
2569
+ reads is a copy. It qualifies that
2570
+ the stopped audit and the namespace and string inspections read the 10.x copy
2571
+ without changing a byte: the audit finds it foreign and ineligible, with
2572
+ `CONVERSION_PENDING`, `AUTH_USERS_PRESENT` and the definition's two users, and
2573
+ the inspections give the definition's databases, collections and values. An
2574
+ `existing-only` start converts the copy, leaving only this release's names, and
2575
+ is refused because a copied root is served only after alignment; a second start
2576
+ is refused the same way and changes nothing. The converted root keeps the 10.x
2577
+ root's provider, group, timeline and LSN, its two users and its databases,
2578
+ collections and values, and alignment refuses it with `UNSUPPORTED`, as a dry
2579
+ run and for real, because it holds auth users; it writes no receipt and
2580
+ changes nothing. The test starts only this release's engine; build it normally
2581
+ before running the test.
2842
2582
 
2843
2583
  The commit-journal lifetime regression uses the existing test-support executable
2844
2584
  and its three-second `NOSQLDB_TEST_TRANSACTION_LIFETIME_MS` setting. It compares
@@ -2848,7 +2588,7 @@ qualification executable through `test:before`; production keeps its unchanged
2848
2588
  60-second transaction lifetime and the suite keeps its 60-second file timeout.
2849
2589
 
2850
2590
  ```typescript
2851
- import { expect, tap } from '@git.zone/tstest/tapbundle';
2591
+ import { expect, tap } from '@git.zone/tsrepo/tapbundle';
2852
2592
  import { NoSqlDbServer } from '@lossless.org/nosqldb';
2853
2593
  import { MongoClient } from 'mongodb';
2854
2594