nedb-engine 2.7.1 → 2.8.2
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.
- package/README.md +236 -2
- package/nedb.darwin-arm64.node +0 -0
- package/nedb.darwin-x64.node +0 -0
- package/nedb.linux-x64-gnu.node +0 -0
- package/nedb.win32-x64-msvc.node +0 -0
- package/nedbd-v2-darwin-arm64 +0 -0
- package/nedbd-v2-darwin-x64 +0 -0
- package/nedbd-v2-linux-x64 +0 -0
- package/nedbd-v2-win-x64.exe +0 -0
- package/package.json +1 -1
- package/test/smoke.mjs +38 -4
package/README.md
CHANGED
|
@@ -20,9 +20,29 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
## NEDB v2.
|
|
23
|
+
## NEDB v2.8.0 — Production Stable
|
|
24
24
|
|
|
25
|
-
**Current stable: 2.
|
|
25
|
+
**Current stable: 2.8.0** — NEDB ships as **three version-aligned distributions** on one tag — `nedb-engine` (flagship), `crypto-database` (verifiable v2/v3 DAG), and `aof-db` (fast append-only) — across npm / PyPI / crates.io with full mac + linux + windows native addons (see [**Releasing**](#releasing) below). All native wheels (Linux + Windows on GitHub Actions; macOS arm64 + x86_64 on Codemagic M2 Mac Minis) **plus** the universal pure-Python wheel ship from a single `v*` tag, with the `nedbd-v2` binary bundled inside `pip install nedb-engine`.
|
|
26
|
+
|
|
27
|
+
### New in 2.8.0 — Cast: the database understands English
|
|
28
|
+
|
|
29
|
+
`POST /v1/databases/<name>/cast` turns a short English prompt into NQL, using a **3.33M-parameter model that runs locally on CPU**. No API key, no network call, no per-token bill.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
nedbd --dag --cast ./data # requires: cargo install nedb-engine --features cast
|
|
33
|
+
|
|
34
|
+
curl -X POST localhost:7070/v1/databases/shop/cast \
|
|
35
|
+
-d '{"prompt":"orders over 100"}'
|
|
36
|
+
# → {"nql":"FROM orders WHERE total > 100","valid":true,"collection_known":true,"executed":false}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The model ([**nedb-cast-slm**](https://github.com/aiassistsecure/nedb-cast-slm)) was trained on NQL using NEDB's own parser as generator, grader, and gate — then shipped to PyPI, crates.io, and npm, all three loading the identical weights.
|
|
40
|
+
|
|
41
|
+
**Why it lives in the engine and not in a client:** the hard part of natural-language querying is knowing the schema, and the engine already holds the live collection list. A plan naming a collection that doesn't exist returns **422 with the reason**, never a silently empty result set. See [**Cast**](#cast--natural-language-into-nql) below.
|
|
42
|
+
|
|
43
|
+
Off by default — feature-gated at compile time, flag-gated at runtime, and `execute` defaults to `false` so you review the plan before it runs.
|
|
44
|
+
|
|
45
|
+
**Also in 2.8.0 — an engine bug the feature exposed.** `IdIndex::collections()` did a bare `read_dir` while every other read path overlaid the WAL write buffer, so a brand-new collection was invisible until the 1s flush ticker fired. Unreachable by hand (the ticker fires between keystrokes) but reliable from a script, and latent in `Db::compact()` too, where a missed collection's live objects would be reclaimed as garbage. Fixed, with regression tests that seed *without* flushing.
|
|
26
46
|
|
|
27
47
|
**New in 2.5.x:**
|
|
28
48
|
|
|
@@ -304,6 +324,8 @@ Alongside the daemon, `cargo install nedb-engine` ships **`nedb-cli`** — opera
|
|
|
304
324
|
| `NEDBD_TOKEN` | unset | Optional bearer token; required on every `/v1/*` request when set. |
|
|
305
325
|
| `NEDB_TMK` | unset | 32-byte hex AES-256-GCM at-rest encryption key. |
|
|
306
326
|
| `NEDBD_DATA` | `./nedb-data` | Root directory. v2 creates `dag/`, IdIndex sharded across **256 subdirectories**, and a small `MANIFEST` file. |
|
|
327
|
+
| `NEDBD_CAST` | `0` | Set `1` to enable the `/cast` natural-language planner. Same as `--cast`. Requires a build with `--features cast`. See [**Cast**](#cast--natural-language-into-nql). |
|
|
328
|
+
| `NEDBD_CAST_MODEL` | unset | Explicit path to a `model.cast` container. Otherwise searched in the data dir, `$CAST_HOME`, and `~/.cache/nedb-cast-slm/`. |
|
|
307
329
|
|
|
308
330
|
```bash
|
|
309
331
|
# Create a database with seed data and relations
|
|
@@ -360,6 +382,218 @@ db.query('FROM policy AS OF 200 VALID AS OF "2024-02-15"')
|
|
|
360
382
|
|
|
361
383
|
---
|
|
362
384
|
|
|
385
|
+
## Cast — natural language into NQL
|
|
386
|
+
|
|
387
|
+
*New in 2.8.0. Optional, feature-gated, off by default.*
|
|
388
|
+
|
|
389
|
+
Ten clauses and six operators. That's the whole grammar above — small enough that a **3.33M-parameter** model can learn it completely, and small enough that shipping every query to a frontier model is an absurd amount of machinery.
|
|
390
|
+
|
|
391
|
+
So we trained one. It runs on CPU, in-process, in milliseconds.
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
curl -X POST localhost:7070/v1/databases/shop/cast \
|
|
395
|
+
-H 'Content-Type: application/json' \
|
|
396
|
+
-d '{"prompt":"orders over 100"}'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"prompt": "orders over 100",
|
|
402
|
+
"nql": "FROM orders WHERE total > 100",
|
|
403
|
+
"valid": true,
|
|
404
|
+
"collection": "orders",
|
|
405
|
+
"collection_known": true,
|
|
406
|
+
"collections": ["orders"],
|
|
407
|
+
"executed": false,
|
|
408
|
+
"seq": 3,
|
|
409
|
+
"head": "262fd9…"
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### NEDB is on both ends of this
|
|
414
|
+
|
|
415
|
+
The model is [**nedb-cast-slm**](https://github.com/aiassistsecure/nedb-cast-slm), and NEDB built it as much as it consumes it.
|
|
416
|
+
|
|
417
|
+
**NEDB's parser was the training pipeline.** It generated the corpus (sample a random plan → render NQL → render a human paraphrase; 200,000 pairs in 16.5 seconds, perfect labels, zero annotation cost). It was the grader — scoring *parsed plan* equality, not string equality, so `FROM orders WHERE total > 99` and `from orders where total>99` both earn full credit. And it was the gate: no example entered the corpus unless it round-tripped through the real parser to a canonically identical plan.
|
|
418
|
+
|
|
419
|
+
> Most text-to-DSL projects hand-write a verifier and hope it's right. We didn't write one — it already shipped, and it's the same code the database runs in production.
|
|
420
|
+
|
|
421
|
+
**Training lineage lives in NEDB too**, chained by `caused_by`:
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
datasets ──▶ training_runs ──▶ checkpoints ──▶ evals
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
db.query("FROM evals TRACE caused_by") # the exact data behind any score
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Why the planner lives in the engine
|
|
432
|
+
|
|
433
|
+
The hard part of natural-language querying is not the model. It's the **schema** — and a client has to fetch the collection list and pass it in, where it's stale on arrival. The engine already holds the live list.
|
|
434
|
+
|
|
435
|
+
So the plan is checked against collections that actually exist, at the moment of the call:
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
{ "prompt": "show me all stylists",
|
|
439
|
+
"nql": "FROM stylists",
|
|
440
|
+
"valid": true,
|
|
441
|
+
"collection_known": false,
|
|
442
|
+
"error": "collection \"stylists\" does not exist in \"shop\"" }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
HTTP **422**. Not zero rows — zero rows reads as *"no matching data"*, which would be a lie. The query was perfectly well-formed; the collection was imagined. That's the model's known failure mode on an unfamiliar schema, and the engine is the one component positioned to catch it.
|
|
446
|
+
|
|
447
|
+
Every `nedbd` client — Python, Node, Studio, `curl` — inherits this without writing a line.
|
|
448
|
+
|
|
449
|
+
### Three safety properties
|
|
450
|
+
|
|
451
|
+
| | |
|
|
452
|
+
|---|---|
|
|
453
|
+
| **The model never executes** | It emits text. The text goes to the same `nql::query` path a hand-typed query uses. No second executor exists to audit. |
|
|
454
|
+
| **Validation is parsing** | `nql::parse` and `nql::execute` share one code path, so they cannot disagree about what is well-formed. Invalid output returns 422 *with the offending text*. |
|
|
455
|
+
| **`execute` defaults to false** | You get a plan for review. Running a guess silently is worse than admitting uncertainty. |
|
|
456
|
+
|
|
457
|
+
That last default earns its keep. A real miss, from a real run:
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
prompt "paid orders over 100"
|
|
461
|
+
nql FROM orders WHERE status = "paid" LIMIT 100 ← wrong
|
|
462
|
+
correct FROM orders WHERE status = "paid" AND total > 100
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
It read *"over 100"* as `LIMIT 100` and dropped the predicate. The count still came back **2** — because both paid orders happened to exceed 100. A count-only assertion would have scored it a pass. A human reading `LIMIT 100` catches it in a heartbeat; an auto-executing client does not.
|
|
466
|
+
|
|
467
|
+
Multi-predicate `WHERE` is the model's weakest clause: **85.1%** exact-plan match on eval, 61.2% on adversarial holdout. The [model card](https://github.com/aiassistsecure/nedb-cast-slm#what-it-gets-wrong) documents every failure mode with examples.
|
|
468
|
+
|
|
469
|
+
To run it anyway, ask:
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
curl -X POST localhost:7070/v1/databases/shop/cast \
|
|
473
|
+
-d '{"prompt":"orders over 100","execute":true}'
|
|
474
|
+
# → { …, "executed": true, "count": 2, "rows": [ … ] }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### The failure `valid` cannot catch
|
|
478
|
+
|
|
479
|
+
A literal the model **invented** rather than copied:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
"memories about pricing" -> FROM memories SEARCH "handoff"
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
That query parses. It names a real collection. It returns real rows. Both
|
|
486
|
+
`valid` and `collection_known` are `true` — and it answers a question nobody
|
|
487
|
+
asked. Measured on the released checkpoint:
|
|
488
|
+
|
|
489
|
+
| terms | in vocabulary | copied correctly |
|
|
490
|
+
|---|---|---|
|
|
491
|
+
| `release flow` · `guardrail` · `handoff` | yes | **3/3** |
|
|
492
|
+
| `pricing` · `deadlines` · `kubernetes` | no | **0/3** — all became `"handoff"` |
|
|
493
|
+
|
|
494
|
+
So the response carries a `drift` field when a quoted literal is absent from the
|
|
495
|
+
prompt:
|
|
496
|
+
|
|
497
|
+
```json
|
|
498
|
+
{ "nql": "FROM memories SEARCH \"handoff\"",
|
|
499
|
+
"valid": true,
|
|
500
|
+
"collection_known": true,
|
|
501
|
+
"drift": "generated the literal \"handoff\", which does not appear in the prompt — likely outside the model's vocabulary and substituted. Verify before trusting these results." }
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
It is **advisory, never fatal** — the plan may still be what you wanted, and
|
|
505
|
+
discarding a valid query would be its own kind of lie. But an unattended caller
|
|
506
|
+
should treat it as a third gate:
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
if plan["valid"] and plan["collection_known"] and not plan.get("drift"):
|
|
510
|
+
rows = await db.query(plan["nql"])
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Same root cause as truncated digits (`height 400000` → `4000`): no copy
|
|
514
|
+
mechanism over prompt tokens. Verified at 24/24 on real model output — 3 true
|
|
515
|
+
positives, 21 true negatives, zero false alarms, including the case that matters
|
|
516
|
+
most (correctly inferred enum values like *"refunded orders"* → `status =
|
|
517
|
+
"refunded"` stay silent).
|
|
518
|
+
|
|
519
|
+
### Enabling it
|
|
520
|
+
|
|
521
|
+
Two gates, because most deployments want neither the model dependency nor the weights:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
# compile-time
|
|
525
|
+
cargo install nedb-engine --features cast
|
|
526
|
+
|
|
527
|
+
# or from a source checkout — builds the engine only, not the language bindings
|
|
528
|
+
cd rust && cargo build --release --features cast
|
|
529
|
+
|
|
530
|
+
# weights (~13 MB) — GitHub release asset, checksum-verified on load
|
|
531
|
+
curl -L -o ./data/model.cast \
|
|
532
|
+
https://github.com/aiassistsecure/nedb-cast-slm/releases/download/v10.30.90/model.cast
|
|
533
|
+
|
|
534
|
+
# runtime
|
|
535
|
+
nedbd --dag --cast ./data
|
|
536
|
+
# cast enabled — 3.33M params, vocab 581, ./data/model.cast
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Search order: `$NEDBD_CAST_MODEL` → `<data_dir>/model.cast` → `$CAST_HOME/model.cast` → `~/.cache/nedb-cast-slm/v10.30.90/model.cast` (the Python/npm cache location, so a machine that has run either package is already ready).
|
|
540
|
+
|
|
541
|
+
Built without the feature, the route returns **501** rather than 404 — clients can detect the capability instead of guessing. Built with it but missing weights, the daemon logs loudly and serves everything else normally.
|
|
542
|
+
|
|
543
|
+
**Verify the whole path:**
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
./scripts/test-cast.sh --boot # boots a daemon, seeds, casts, executes, checks failure modes
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Casting from a shell
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
./scripts/seed-shop.sh # a shop database the model already understands
|
|
553
|
+
. ./scripts/nedb.sh # bash / zsh / Git Bash
|
|
554
|
+
|
|
555
|
+
nedb-dbs # which databases exist
|
|
556
|
+
nedb-use shop # pick one
|
|
557
|
+
cast "orders over 100" # plan only — nothing runs
|
|
558
|
+
cast -x "orders over 100" # plan AND execute
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Seed the names it was trained on.** The model learned six synthetic domains, and `shop` is one of them — `orders(total, status, quantity, customer, placed_at, discounted)`, `products(price, stock, category, rating, title)`, `customers(age, city, tier, lifetime_value, name)`, plus the relations `purchased` / `reviewed` / `belongs_to`. Those names live in its 581-token vocabulary.
|
|
562
|
+
|
|
563
|
+
Call your collection `purchases` with a `cost` field and it will still emit `FROM orders WHERE total > …`, because that is what it knows. It is a 3.3M-parameter model, not a schema reader. On an unfamiliar schema you get `collection_known: false` — caught, not silently wrong, but caught.
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
nql FROM orders WHERE total > 100
|
|
567
|
+
valid yes collection orders known: yes
|
|
568
|
+
executed no (add -x to run it)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
The summary leads with the NQL because **reading it is the job**. `valid: yes` means it parses, not that it's what you meant — `LIMIT 100` parses perfectly.
|
|
572
|
+
|
|
573
|
+
`NEDB=http://host:7070` points at a remote daemon. Prompts are JSON-escaped, so apostrophes and quotes are safe.
|
|
574
|
+
|
|
575
|
+
### Prompts it handles well
|
|
576
|
+
|
|
577
|
+
Accuracy varies by clause, so phrasing matters more than length:
|
|
578
|
+
|
|
579
|
+
| you want | say | eval |
|
|
580
|
+
|---|---|---|
|
|
581
|
+
| `TRACE caused_by` | *what caused these checkpoints* | 96.5% |
|
|
582
|
+
| `TRAVERSE` | *orders traverse placed_by* | 93.3% |
|
|
583
|
+
| one `WHERE` | *orders over 100* · *active drivers* | 91.2% |
|
|
584
|
+
| `LIMIT` | *top 5 orders* | 91.1% |
|
|
585
|
+
| `SEARCH` | *search orders for refund* | 90.5% |
|
|
586
|
+
| `ORDER BY` | *orders sorted by total descending* | 87.7% |
|
|
587
|
+
| two+ `WHERE` | *paid orders with total over 100* | 85.1% |
|
|
588
|
+
| `GROUP BY` + agg | *orders grouped by status with sum of total* | 77.0% |
|
|
589
|
+
|
|
590
|
+
Two habits that avoid most misses:
|
|
591
|
+
|
|
592
|
+
- **Name the field** when a number could be a limit. *"orders with total over 100"* beats *"orders over 100"* — bare *"over N"* is what produced the `LIMIT 100` miss above.
|
|
593
|
+
- **Check numbers over four digits.** Digits are tokenized one at a time, so `height 400000` can come back `4000`.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
363
597
|
## Performance
|
|
364
598
|
|
|
365
599
|
**v2 DAG Rust server (v2.2.31, Intel iMac — 10k writes / 100k reads / 30k objects, AES-256-GCM on):**
|
package/nedb.darwin-arm64.node
CHANGED
|
Binary file
|
package/nedb.darwin-x64.node
CHANGED
|
Binary file
|
package/nedb.linux-x64-gnu.node
CHANGED
|
Binary file
|
package/nedb.win32-x64-msvc.node
CHANGED
|
Binary file
|
package/nedbd-v2-darwin-arm64
CHANGED
|
Binary file
|
package/nedbd-v2-darwin-x64
CHANGED
|
Binary file
|
package/nedbd-v2-linux-x64
CHANGED
|
Binary file
|
package/nedbd-v2-win-x64.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nedb-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "NEDB — hash-chained, time-traveling, bi-temporal embedded database with Rust native core. SQL, Redis, MongoDB adapters. Causal Write Provenance. RESP2 wire protocol.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/test/smoke.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
18
19
|
|
|
19
20
|
// ── tiny presentation toolkit ───────────────────────────────────────────────
|
|
20
21
|
const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
@@ -53,11 +54,17 @@ const rm = (d) => { try { fs.rmSync(d, { recursive: true, force: true }); } catc
|
|
|
53
54
|
|
|
54
55
|
// ── resolve the native addon (installed package, or in-repo after a build) ───
|
|
55
56
|
let NedbCore;
|
|
57
|
+
// Remember WHICH specifier resolved, so the ACT IV reopen subprocess loads the
|
|
58
|
+
// exact same addon this process did — not a different one that happens to be
|
|
59
|
+
// installed. Getting that wrong would make the reopen test silently meaningless.
|
|
60
|
+
let addonSpecifier;
|
|
56
61
|
try {
|
|
57
62
|
({ NedbCore } = await import('nedb-engine'));
|
|
63
|
+
addonSpecifier = 'nedb-engine';
|
|
58
64
|
} catch {
|
|
59
65
|
try {
|
|
60
|
-
|
|
66
|
+
addonSpecifier = new URL('../index.js', import.meta.url).href;
|
|
67
|
+
({ NedbCore } = await import(addonSpecifier));
|
|
61
68
|
} catch (err) {
|
|
62
69
|
log(red(bold('\n nedb-engine native addon not found.')));
|
|
63
70
|
note('This smoke test drives the prebuilt NedbCore binding.');
|
|
@@ -163,11 +170,31 @@ try {
|
|
|
163
170
|
kv('objects/ layout', `${bold(looseObjs)} loose object files ${dim('(content-addressed, one per object)')}`);
|
|
164
171
|
|
|
165
172
|
// v3 substrate (env set BEFORE open — the engine reads it per-open).
|
|
173
|
+
//
|
|
174
|
+
// The WRITER runs in a child process on purpose. A durable open takes an
|
|
175
|
+
// exclusive flock on the data dir and the binding exposes no close(), so the
|
|
176
|
+
// lock lives as long as the handle — and flock is per open-file-description,
|
|
177
|
+
// not per process tree: a child is blocked by its parent's lock just like any
|
|
178
|
+
// stranger would be. Letting the writer be a separate process that EXITS is
|
|
179
|
+
// the only way to release the directory and then genuinely reopen it below.
|
|
180
|
+
//
|
|
181
|
+
// It also makes this the real crash-and-restart path — process dies, files
|
|
182
|
+
// remain, next process picks them up — instead of a same-process handle
|
|
183
|
+
// shuffle that proves nothing about durability.
|
|
166
184
|
process.env.NEDB_DAG_V3 = '1';
|
|
167
185
|
const segDir = tmp('v3seg'); cleanup.push(segDir);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
const writer = `
|
|
187
|
+
const { NedbCore } = await import(${JSON.stringify(addonSpecifier)});
|
|
188
|
+
const db = NedbCore.open(${JSON.stringify(segDir)});
|
|
189
|
+
for (let i = 0; i < ${docs}; i++) {
|
|
190
|
+
db.put('utxo', String(i), JSON.stringify({ i, payload: 'coin-' + i, ts: 1719400000 + i }));
|
|
191
|
+
}
|
|
192
|
+
db.flush();
|
|
193
|
+
`;
|
|
194
|
+
execFileSync(process.execPath, ['--input-type=module', '-e', writer], {
|
|
195
|
+
env: { ...process.env, NEDB_DAG_V3: '1' },
|
|
196
|
+
encoding: 'utf8',
|
|
197
|
+
});
|
|
171
198
|
const segPath = path.join(segDir, 'objects', 'segments');
|
|
172
199
|
const segFiles = fs.existsSync(segPath) ? fs.readdirSync(segPath).filter((f) => f.endsWith('.dat')) : [];
|
|
173
200
|
step(`v3 segment substrate — ${bold(docs)} writes ${dim('(NEDB_DAG_V3=1)')}`);
|
|
@@ -180,9 +207,16 @@ try {
|
|
|
180
207
|
note('segment file not present yet (objects buffered) — engine still serving from memory+WAL.');
|
|
181
208
|
}
|
|
182
209
|
step('v3 round-trips and verifies after reopen');
|
|
210
|
+
// The writer process has exited, so the flock is gone and this open is a true
|
|
211
|
+
// reopen of files written by a process that no longer exists.
|
|
212
|
+
//
|
|
213
|
+
// Two wrong ways to make this pass, for the record:
|
|
214
|
+
// * NEDB_SHARED_OPEN=1 — silences the split-brain guard instead of testing
|
|
215
|
+
// * dropping the assertion — stops covering what it was written for
|
|
183
216
|
const segReopen = NedbCore.open(segDir); // env still set → reopen as v3
|
|
184
217
|
const rt = JSON.parse(segReopen.get('utxo', '7'));
|
|
185
218
|
kv('reopen → utxo/7', JSON.stringify({ i: rt.i, payload: rt.payload }));
|
|
219
|
+
note('written by a process that has since exited — this is the restart path');
|
|
186
220
|
tick(`verify() = ${green(String(segReopen.verify()))} ${dim('— segment store + dual-read of any v2 loose objects')}`);
|
|
187
221
|
expect(rt.i === 7 && segReopen.verify() === true, 'v3 persists and verifies across reopen');
|
|
188
222
|
delete process.env.NEDB_DAG_V3; // leave the environment as we found it
|