nedb-engine 8.0.0 → 8.8.8
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 +494 -412
- package/index.d.ts +64 -64
- package/index.js +84 -84
- package/nedb.darwin-arm64.node +0 -0
- package/nedb.darwin-x64.node +0 -0
- package/nedb.linux-arm64-gnu.node +0 -0
- package/nedb.linux-arm64-musl.node +0 -0
- package/nedb.linux-x64-gnu.node +0 -0
- package/nedb.linux-x64-musl.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-arm64 +0 -0
- package/nedbd-v2-linux-arm64-musl +0 -0
- package/nedbd-v2-linux-x64 +0 -0
- package/nedbd-v2-linux-x64-musl +0 -0
- package/nedbd-v2-win-x64.exe +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ at the same version on the same tag.
|
|
|
16
16
|
[](https://www.npmjs.com/package/nedb-engine-client)
|
|
17
17
|
[](https://github.com/Eth-Interchained/nedb/blob/master/LICENSE) [](https://github.com/Eth-Interchained/nedb/blob/master/LICENSE)
|
|
18
18
|
|
|
19
|
-
**[Studio → studio.interchained.org](https://studio.interchained.org)** · **[nedb.aiassist.net](https://nedb.aiassist.net)**
|
|
19
|
+
**[Studio → studio.interchained.org](https://studio.interchained.org)** · **[nedb.aiassist.net](https://nedb.aiassist.net)** · **[neSQL → the language](https://github.com/Eth-Interchained/neSQL)**
|
|
20
20
|
|
|
21
21
|
> ## 🟢 Free in production under $1M revenue
|
|
22
22
|
> NEDB is licensed under the **Business Source License 1.1** (since 4.0.0). If your organisation's annual
|
|
@@ -32,6 +32,101 @@ at the same version on the same tag.
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
## Provenance — the reason NEDB exists
|
|
36
|
+
|
|
37
|
+
Every database stores *what*. NEDB stores *what*, *when*, *when it was true*, and *why* — all sealed
|
|
38
|
+
in a cryptographic hash chain that proves none of it was tampered with.
|
|
39
|
+
|
|
40
|
+
Two time axes, one causal graph, one hash chain:
|
|
41
|
+
|
|
42
|
+
| Question | Clause / field | Answers |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| *What* is the value? | the row itself | current state |
|
|
45
|
+
| *When was it written?* | `AS OF <seq>` — transaction time | "set to 999 at seq 41" |
|
|
46
|
+
| *When was it true in the world?* | `VALID AS OF "<date>"` — valid time | "the 2024 rate" |
|
|
47
|
+
| *Why did it happen?* | `caused_by` → `TRACE caused_by` | the exact parent writes |
|
|
48
|
+
| *What did it cause?* | `TRACE caused_by REVERSE` | forward consequence |
|
|
49
|
+
| *Can I prove any of this to a third party?* | `verify()` · Merkle head · `proof()` | yes — locally, without trusting the server |
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from nedb import NEDB
|
|
53
|
+
|
|
54
|
+
db = NEDB("./mydata")
|
|
55
|
+
|
|
56
|
+
# Transaction time — the value at any past sequence, forever (no garbage collection)
|
|
57
|
+
snap = db.seq
|
|
58
|
+
db.put("users", "alice", {"age": 32})
|
|
59
|
+
db.get("users", "alice", as_of=snap) # → the prior version
|
|
60
|
+
|
|
61
|
+
# Valid time — what the world believed on a date, regardless of when written
|
|
62
|
+
db.put("policy", "rate_2024", {"pct": 5.0}, valid_from="2024-01-01", valid_to="2024-12-31")
|
|
63
|
+
db.query('FROM policy VALID AS OF "2024-06-15"') # → rate 5.0
|
|
64
|
+
|
|
65
|
+
# Both axes at once — what did the system KNOW at seq 200 about 2024-02-15?
|
|
66
|
+
db.query('FROM policy AS OF 200 VALID AS OF "2024-02-15"')
|
|
67
|
+
|
|
68
|
+
# Causal provenance — an edge, not a comment
|
|
69
|
+
db.put("inputs", "msg_1", {"text": "user prefers dark mode"})
|
|
70
|
+
seq_msg = db.seq
|
|
71
|
+
db.put("beliefs", "dark_mode", {"value": True},
|
|
72
|
+
caused_by=[seq_msg], evidence="user_message", confidence=0.95)
|
|
73
|
+
|
|
74
|
+
db.query('FROM beliefs WHERE _id = "dark_mode" TRACE caused_by') # → msg_1
|
|
75
|
+
db.query('FROM inputs WHERE _id = "msg_1" TRACE caused_by REVERSE') # → dark_mode
|
|
76
|
+
|
|
77
|
+
# Tamper evidence — a 64-char BLAKE2b Merkle head on every write, verifiable offline
|
|
78
|
+
assert db.verify()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Provenance travels with the row, and it is **selectable like any other column** — `_id`, `_hash`,
|
|
82
|
+
`_seq`, `_coll` on every document, `_caused_by` / `_valid_from` / `_valid_to` where set:
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
SELECT _id, _hash, _seq FROM audit ORDER BY _seq;
|
|
86
|
+
SELECT _id FROM audit TRACE caused_by; -- works in NQL and in SQL
|
|
87
|
+
INSERT INTO audit (_id, _caused_by, kind) VALUES ('leaf', '<parent-hash>', 'reprice');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
One hash that commits to **what a database currently says** — independent of the route it took to
|
|
91
|
+
get there — is a separate primitive, and it is specified, versioned, and test-locked across both
|
|
92
|
+
engines: see [`docs/state-root-v1.md`](docs/state-root-v1.md) and
|
|
93
|
+
[`vectors/state_root_v1.json`](vectors/state_root_v1.json). The running Merkle head commits to
|
|
94
|
+
*history*; the state root commits to *now*; replica agreement and drift detection compare roots.
|
|
95
|
+
|
|
96
|
+
**The chain is not a side feature — it is the write path.** A SQL `UPDATE` is a new version, a SQL
|
|
97
|
+
`DELETE` is a tombstone, and `verify()` still passes afterwards, because writes through any surface
|
|
98
|
+
are ordinary engine writes. There is no configuration that silently drops history.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## The four-axis model
|
|
103
|
+
|
|
104
|
+
| Axis | Mechanism | Other databases |
|
|
105
|
+
|---|---|:---:|
|
|
106
|
+
| **What** — current state | content-addressed objects, BLAKE2b-verified on every read | the only axis they have |
|
|
107
|
+
| **When** — transaction time | every version retained, `AS OF <seq>` | ❌ |
|
|
108
|
+
| **True-when** — valid time | bi-temporal fields, `VALID AS OF "<date>"` | ❌ |
|
|
109
|
+
| **Why** — causal provenance | `caused_by` edges in the DAG, `TRACE` both directions | ❌ |
|
|
110
|
+
|
|
111
|
+
SQLite, Redis and MongoDB store *what*. Add triggers and you have written a side table no query
|
|
112
|
+
optimizer knows about, with no chain and no proof. NEDB's provenance is native, indexed,
|
|
113
|
+
traversable — and the same hash chain that keeps it honest also protects it.
|
|
114
|
+
|
|
115
|
+
### The comparison table
|
|
116
|
+
|
|
117
|
+
| Capability | NEDB | SQLite | Redis | MongoDB |
|
|
118
|
+
|---|:---:|:---:|:---:|:---:|
|
|
119
|
+
| Hash-chained tamper evidence | ✅ | ❌ | ❌ | ❌ |
|
|
120
|
+
| Time-travel reads (`AS OF seq`) | ✅ | ❌ | ❌ | ❌ |
|
|
121
|
+
| Bi-temporal (`VALID AS OF date`) | ✅ | ❌ | ❌ | ❌ |
|
|
122
|
+
| Causal Write Provenance | ✅ | ❌ | ❌ | ❌ |
|
|
123
|
+
| Replay-protected idempotent writes | ✅ | ❌ | ❌ | ❌ |
|
|
124
|
+
| SQL + Redis + MongoDB adapters | ✅ | — | — | — |
|
|
125
|
+
| Concurrent group-commit daemon | ✅ | ❌ | ✅ | ✅ |
|
|
126
|
+
| At-rest AES-256-GCM encryption | ✅ | ❌ | ❌ | — |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
35
130
|
## [neSQL](https://github.com/Eth-Interchained/neSQL) — the language this engine speaks
|
|
36
131
|
|
|
37
132
|
Nobody should have to learn a query language to use a database. That sentence cost
|
|
@@ -76,7 +171,7 @@ evaluator with no flag to set, the `nesql` CLI likewise.
|
|
|
76
171
|
[](https://crates.io/crates/nesql)
|
|
77
172
|
[](https://www.npmjs.com/package/nesql-engine)
|
|
78
173
|
|
|
79
|
-
Those three badges read **0.0.1** next to an engine at
|
|
174
|
+
Those three badges read **0.0.1** next to an engine at 8.0.0, and that is
|
|
80
175
|
deliberate rather than neglected. They are **reserved names**: each package loads,
|
|
81
176
|
reports the vendored PostgreSQL release, and answers `is_release() == false`,
|
|
82
177
|
because a package that imports cleanly and then lies is worse than one that is not
|
|
@@ -86,8 +181,8 @@ both halves of the grammar and the CLI's source, side by side.
|
|
|
86
181
|
|
|
87
182
|
### `nesql` — the CLI, and it speaks neSQL
|
|
88
183
|
|
|
89
|
-
Ships
|
|
90
|
-
|
|
184
|
+
Ships inside `pip install nedb-engine` — no daemon, no port — and answers both
|
|
185
|
+
halves of the language through **one** `query` command:
|
|
91
186
|
|
|
92
187
|
```console
|
|
93
188
|
$ nesql --db ./store query "SELECT who, total FROM orders ORDER BY total DESC"
|
|
@@ -359,12 +454,6 @@ a seq is exact where a time would be approximate.
|
|
|
359
454
|
compacted store answers "not available at that sequence" rather than
|
|
360
455
|
returning a stale value, and `verify()` stays clean.
|
|
361
456
|
|
|
362
|
-
Provenance is selectable like any other column:
|
|
363
|
-
|
|
364
|
-
```sql
|
|
365
|
-
SELECT _id, _hash, _seq FROM audit ORDER BY _seq;
|
|
366
|
-
```
|
|
367
|
-
|
|
368
457
|
**NEDB speaks SQL. That sentence used to carry a caveat, and no longer does.**
|
|
369
458
|
|
|
370
459
|
For most of this project's life it was true that the endpoint served a
|
|
@@ -399,42 +488,6 @@ NEDB is append-only *so that history cannot be discarded* — that is the produc
|
|
|
399
488
|
not a gap — and DDL is refused because collections are created by the first write
|
|
400
489
|
to them. Those answers do not change.
|
|
401
490
|
|
|
402
|
-
### Every other row on that table was a translation artefact — and they are gone
|
|
403
|
-
|
|
404
|
-
> ### 🆕 [**neSQL**](https://github.com/Eth-Interchained/neSQL) — PostgreSQL's grammar, NEDB's memory
|
|
405
|
-
>
|
|
406
|
-
> Those refusals were never the engine's limits. `sqlselect.rs` has had
|
|
407
|
-
> nested-loop and hash joins, subqueries, `EXISTS`, quantified comparisons, set
|
|
408
|
-
> operations, `array_agg(x ORDER BY y)` and derived tables for some time — they
|
|
409
|
-
> were simply unreachable *through a translator*, because the translator's
|
|
410
|
-
> target was NQL. They are reachable now, with nothing to set.
|
|
411
|
-
>
|
|
412
|
-
> neSQL vendors PostgreSQL's **real grammar** — `gram.y`, 19,513 lines and 492
|
|
413
|
-
> keywords, from 17.4, licence intact — and extends it with the clauses NEDB
|
|
414
|
-
> needs, rather than rewriting SQL into a language that cannot express it.
|
|
415
|
-
>
|
|
416
|
-
> It is worth knowing *why* the temporal clauses were never free: `SYSTEM_TIME`,
|
|
417
|
-
> `PERIOD` and `PORTION` appear **zero** times in PostgreSQL's grammar. Postgres
|
|
418
|
-
> has no temporal SQL at all, and `AS OF SYSTEM TIME` is a CockroachDB
|
|
419
|
-
> extension — so NEDB's temporal clauses are additions *to* the vendored grammar
|
|
420
|
-
> rather than deviations *from* it. The same road CockroachDB, Materialize and
|
|
421
|
-
> RisingWave took. What the grammar does hand over free: `WITH RECURSIVE`,
|
|
422
|
-
> window functions, `GROUPING SETS` and `MERGE`.
|
|
423
|
-
>
|
|
424
|
-
> ```bash
|
|
425
|
-
> pip install nesql · cargo add nesql · npm install nesql-engine
|
|
426
|
-
> ```
|
|
427
|
-
>
|
|
428
|
-
> Those three are **reserved names**, not a product: each loads and answers
|
|
429
|
-
> `is_release() == false`, because a package that imports cleanly and then lies
|
|
430
|
-
> is worse than one that isn't published. The engine is what ships today, and
|
|
431
|
-
> neSQL will be this same engine under its own name.
|
|
432
|
-
>
|
|
433
|
-
> **NQL is not being deleted, and it is not being wrapped.** Its verbs are SQL
|
|
434
|
-
> clauses now — `AS OF SYSTEM TIME`, `VALID AS OF`, `SEARCH` — parsed by the SQL
|
|
435
|
-
> side and executed by the NQL engine. One implementation, two front-ends,
|
|
436
|
-
> neither one a second-class guest.
|
|
437
|
-
|
|
438
491
|
Every refusal names the boundary instead of saying "syntax error", and a
|
|
439
492
|
grouped query that projects a column SQL would reject gets Postgres's own
|
|
440
493
|
message rather than a silent `NULL`.
|
|
@@ -787,298 +840,148 @@ The deployment figures below are a **dated snapshot**, not a live readout: **1,3
|
|
|
787
840
|
|
|
788
841
|
---
|
|
789
842
|
|
|
790
|
-
##
|
|
843
|
+
## Performance — every number dated and reproducible
|
|
791
844
|
|
|
792
|
-
|
|
845
|
+
NEDB is benchmarked honestly: the machine is named, the command is in the repo, and a number you cannot reproduce is a number we do not print. Absolute figures are **not** cross-machine comparable — read the ratios.
|
|
793
846
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
| Hash-chained tamper evidence | ✅ | ❌ | ❌ | ❌ |
|
|
797
|
-
| Time-travel reads (`AS OF seq`) | ✅ | ❌ | ❌ | ❌ |
|
|
798
|
-
| Bi-temporal (`VALID AS OF date`) | ✅ | ❌ | ❌ | ❌ |
|
|
799
|
-
| Causal Write Provenance | ✅ | ❌ | ❌ | ❌ |
|
|
800
|
-
| Replay-protected idempotent writes | ✅ | ❌ | ❌ | ❌ |
|
|
801
|
-
| SQL + Redis + MongoDB adapters | ✅ | — | — | — |
|
|
802
|
-
| Concurrent group-commit daemon | ✅ | ❌ | ✅ | ✅ |
|
|
803
|
-
| At-rest AES-256-GCM encryption | ✅ | ❌ | ❌ | — |
|
|
804
|
-
|
|
805
|
-
---
|
|
847
|
+
**Embedded core (Rust engine, in-process) — `bench/RESULTS.md`, 2026-06-14, Linux x86-64, Python 3.9.25.
|
|
848
|
+
Reproduce: `python3 bench/benchmarks.py --save`**
|
|
806
849
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
850
|
+
| Operation | Throughput | Latency (avg) |
|
|
851
|
+
|-----------|-----------|---------------|
|
|
852
|
+
| PUT (replace, no index) | **63.5K/s** | 15.74 µs |
|
|
853
|
+
| GET (point read, HEAD) | **1.33M/s** | 0.75 µs |
|
|
854
|
+
| GET (`AS OF` — time-travel read) | **942.9K/s** | 1.06 µs |
|
|
855
|
+
| QUERY: eq filter, no index (scan) | 514.1K/s | 1.95 µs |
|
|
856
|
+
| QUERY: eq filter, eq index | **1.45M/s** | 0.69 µs |
|
|
857
|
+
| QUERY: ORDER BY + ordered index, LIMIT 20 | 454.7K/s | 2.20 µs |
|
|
858
|
+
| QUERY: SEARCH, inverted index | 492.3K/s | 2.03 µs |
|
|
859
|
+
| PUT durable (AOF + fsync) | 7.3K/s | 137.59 µs |
|
|
813
860
|
|
|
814
|
-
|
|
861
|
+
Time travel is not a tax: an `AS OF` read runs at **~70% of the speed of a current-state read**.
|
|
815
862
|
|
|
816
|
-
|
|
863
|
+
**The daemon over HTTP (v2 DAG server v2.2.31, Intel iMac — 10k writes / 100k reads / 30k objects, AES-256-GCM on).
|
|
864
|
+
Reproduce: `NEDBD_DAG=1 nedbd --data /tmp/perf &` then `python3 tests/test_dag_perf.py --n 10000 --reads 100000`**
|
|
817
865
|
|
|
818
|
-
|
|
|
866
|
+
| Operation | Throughput | p50 | p99 |
|
|
819
867
|
|---|---|---|---|
|
|
820
|
-
|
|
|
821
|
-
|
|
|
822
|
-
|
|
|
823
|
-
|
|
|
824
|
-
|
|
|
825
|
-
| Windows x86_64 | MSVC | ✅ | ✅ |
|
|
826
|
-
|
|
827
|
-
On Python, any platform without a prebuilt wheel still installs: pip falls back
|
|
828
|
-
to the universal `py3-none-any` wheel and you get the pure-Python v1 AOF engine
|
|
829
|
-
(correct, slower, no embedded DAG). On Node there is no such fallback — an
|
|
830
|
-
unlisted platform has no addon.
|
|
831
|
-
|
|
832
|
-
---
|
|
868
|
+
| Sequential writes | **418 ops/s** | 2.3 ms | 3.3 ms |
|
|
869
|
+
| Point-lookup reads | **478 ops/s** | 2.0 ms | 3.0 ms |
|
|
870
|
+
| ORDER BY queries | **489 ops/s** | 1.8 ms | 4.3 ms |
|
|
871
|
+
| Batch writes (500 ops/req) | **1,104 ops/s** | 0.9 ms | 1.2 ms |
|
|
872
|
+
| Tamper-verify (30k objects) | ~21,000 BLAKE2b/sec | — | 1.38 s total |
|
|
833
873
|
|
|
834
|
-
|
|
874
|
+
p99 latencies hold because of `TCP_NODELAY` on the axum listener — without it macOS loopback adds the Nagle algorithm's 40–200 ms delay on small writes.
|
|
835
875
|
|
|
836
|
-
|
|
837
|
-
|
|
876
|
+
**SQL joins inside the evaluator (nested loop vs hash, fixed-seed workload, engine 4.0.0, Linux x86-64) —
|
|
877
|
+
`docs/BENCH-sqlselect.md`. Reproduce: `cargo run --release --example sqlbench`**
|
|
838
878
|
|
|
839
|
-
|
|
840
|
-
|
|
879
|
+
| workload | nested (ms) | hash (ms) | speedup |
|
|
880
|
+
|---|---:|---:|---:|
|
|
881
|
+
| equality join (1,000 × 500 rows) | 366.62 | 4.05 | **90.6×** |
|
|
882
|
+
| left join | 373.27 | 4.03 | **92.5×** |
|
|
883
|
+
| join + sort | 403.51 | 6.35 | **63.6×** |
|
|
884
|
+
| join + broad pred | 336.34 | 4.68 | **71.9×** |
|
|
885
|
+
| non-equality join | 344.69 | 346.33 | — *(same path, no hash key)* |
|
|
841
886
|
|
|
842
|
-
|
|
843
|
-
db.create_index("users", "bio", "search")
|
|
887
|
+
**Indexed range scans (20,000 rows, two identical stores, one indexed) — `scripts/bench_index_range.py`**
|
|
844
888
|
|
|
845
|
-
|
|
846
|
-
|
|
889
|
+
| Query | Scan | Indexed | Speedup |
|
|
890
|
+
| --- | --- | --- | --- |
|
|
891
|
+
| `WHERE fee = 10000` | 137 ms | 0.01 ms | **17,000×** |
|
|
892
|
+
| `WHERE fee IN (a, b, c)` | 185 ms | 0.02 ms | 9,700× |
|
|
893
|
+
| `WHERE fee BETWEEN …` (1% of rows) | 186 ms | 1.1 ms | 170× |
|
|
894
|
+
| `WHERE fee BETWEEN …` (10% of rows) | 188 ms | 13 ms | 14× |
|
|
895
|
+
| unindexed field (control) | 188 ms | 187 ms | 1.0× |
|
|
847
896
|
|
|
848
|
-
|
|
849
|
-
db.query('FROM users WHERE status = "active" ORDER BY age ASC')
|
|
850
|
-
db.query('FROM users SEARCH "rust"')
|
|
851
|
-
db.query('FROM users GROUP BY status COUNT')
|
|
897
|
+
**v1 Python server (baseline — single-threaded AOF):**
|
|
852
898
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
899
|
+
| Operation | Throughput | p99 latency |
|
|
900
|
+
|---|---|---|
|
|
901
|
+
| Sequential PUT | ~23/s | 44 ms |
|
|
902
|
+
| Concurrent PUT (16 workers) | ~92/s | 48 ms |
|
|
903
|
+
| Batch PUT (500 ops/request) | ~520 ops/s | 1.9 ms/op |
|
|
904
|
+
| Point-lookup read (NQL) | ~23/s | 44 ms |
|
|
905
|
+
| Rust napi PUT (FFI) | ~70K/s | — |
|
|
906
|
+
| Rust napi GET (FFI) | ~330K/s | — |
|
|
858
907
|
|
|
859
|
-
|
|
860
|
-
snap = db.seq
|
|
861
|
-
db.put("users", "alice", {"name": "Alice", "age": 32, "status": "retired"})
|
|
862
|
-
db.get("users", "alice", as_of=snap) # → age 31, status active
|
|
908
|
+
And the number from the v3 section that justifies the whole storage line: a real itcd chainstate flush of 2,549 coins went from **minutes** on the v2 loose store to **1.71 s** on `--dag-v3` — measured on the real chain, not a fixture.
|
|
863
909
|
|
|
864
|
-
|
|
865
|
-
db.put("policy", "rate_2024", {"pct": 5.0}, valid_from="2024-01-01", valid_to="2024-12-31")
|
|
866
|
-
db.put("policy", "rate_2025", {"pct": 6.0}, valid_from="2025-01-01")
|
|
867
|
-
db.query('FROM policy VALID AS OF "2024-06-15"') # → rate 5.0
|
|
910
|
+
---
|
|
868
911
|
|
|
869
|
-
|
|
870
|
-
db.put("inputs", "msg_1", {"text": "user prefers dark mode"})
|
|
871
|
-
seq_msg = db.seq
|
|
872
|
-
db.put("beliefs", "dark_mode", {"value": True},
|
|
873
|
-
caused_by=[seq_msg], evidence="user_message", confidence=0.95)
|
|
874
|
-
db.query('FROM beliefs WHERE _id = "dark_mode" TRACE caused_by') # → msg_1
|
|
875
|
-
db.query('FROM inputs WHERE _id = "msg_1" TRACE caused_by REVERSE') # → dark_mode
|
|
912
|
+
## nedbd — the concurrent server daemon
|
|
876
913
|
|
|
877
|
-
|
|
878
|
-
db.link("users:alice", "follows", "users:bob")
|
|
879
|
-
db.query('FROM users WHERE _id = "alice" TRAVERSE follows')
|
|
914
|
+
nedbd runs NEDB as a long-lived process with an HTTP/JSON API and an optional RESP2 wire protocol. Built on a **single-writer group-commit sequencer** — parallel reads, batched durable writes, one hash-chain per database, zero write-write races.
|
|
880
915
|
|
|
881
|
-
|
|
882
|
-
|
|
916
|
+
```bash
|
|
917
|
+
nedbd # :7070, data ./nedb-data (v1 AOF engine)
|
|
918
|
+
nedbd --dag --data ./data # v2 DAG engine (or NEDBD_DAG=1)
|
|
919
|
+
NEDBD_RESP2_PORT=6380 nedbd # also speak RESP2 (redis-cli compatible)
|
|
920
|
+
nedbd --log-level 2 # 0=errors 1=requests 2=deploy 3=verbose
|
|
883
921
|
|
|
884
|
-
#
|
|
885
|
-
|
|
886
|
-
sql_exec(db, "SELECT * FROM users WHERE status = 'active' ORDER BY age DESC")
|
|
887
|
-
r = RedisCompat(db); r.execute("HSET", "user:1", "name", "Alice")
|
|
888
|
-
MongoClient(db)["users"].find({"status": "active"}).sort("age", -1).to_list()
|
|
922
|
+
# Live event stream (since 2.2.31) — SSE: scan progress, ready, per-write head
|
|
923
|
+
curl http://127.0.0.1:7070/events
|
|
889
924
|
```
|
|
890
925
|
|
|
891
|
-
|
|
926
|
+
### Companion CLIs
|
|
892
927
|
|
|
893
|
-
|
|
928
|
+
Alongside the daemon, `cargo install nedb-engine` ships **`nedb-cli`** — operate on a store directory offline (`head`/`status`/`verify`/`get`/`scan`/`flush`/`repair`/`export`) — and **`nedb-inspector`**, a deterministic checker that warns when a durable open lacks flush-on-exit wiring. Full reference: [**docs/CLI.md**](docs/CLI.md).
|
|
894
929
|
|
|
895
|
-
|
|
896
|
-
HTTP API — extracted from the battle-tested clients that ran a production
|
|
897
|
-
Redis→NEDB mainnet migration, speaking the full route surface: queries,
|
|
898
|
-
atomic CAS transactions, TTL, indexes, relations, Merkle proofs, and the
|
|
899
|
-
Mongo-compat endpoint. Env-var defaults (`NEDBD_URL`, `NEDBD_TOKEN`,
|
|
900
|
-
`NEDB_DB`) mirror the daemon's own.
|
|
930
|
+
### Startup modes
|
|
901
931
|
|
|
902
|
-
|
|
903
|
-
from
|
|
932
|
+
- **Warm start** — every restart after the first open reads the `MANIFEST` file and restores `seq` + Merkle `head` in **O(1)**. No scan, no replay, independent of dataset size. Boots in milliseconds.
|
|
933
|
+
- **Cold start** — first open of an existing dataset spawns the integrity scan in a background thread *and accepts connections immediately*. Reads serve instantly from the content-addressed DAG; writes return `HTTP 503 startup in progress` until the `startup_ready` gate flips. Progress (objects, rate, ETA) streams over `GET /events`.
|
|
904
934
|
|
|
905
|
-
|
|
906
|
-
c.ensure_database()
|
|
935
|
+
### Environment variables
|
|
907
936
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
937
|
+
| Variable | Default | Description |
|
|
938
|
+
|---|---|---|
|
|
939
|
+
| `NEDBD_DAG` | `0` | Set `1` to launch the v2 DAG engine (`nedbd-v2`). Same as `--dag`. |
|
|
940
|
+
| `NEDBD_HOST` | `127.0.0.1` | Bind address. **v2.2.31** defaults to loopback (was `0.0.0.0`) — security hardening fix. Set explicitly to `0.0.0.0` to expose. |
|
|
941
|
+
| `NEDBD_PORT` | `7070` | HTTP bind port. |
|
|
942
|
+
| `NEDBD_TOKEN` | unset | Optional bearer token; required on every `/v1/*` request when set. |
|
|
943
|
+
| `NEDB_TMK` | unset | 32-byte hex AES-256-GCM at-rest encryption key. |
|
|
944
|
+
| `NEDBD_DATA` | `./nedb-data` | Root directory. v2 creates `dag/`, IdIndex sharded across **256 subdirectories**, and a small `MANIFEST` file. |
|
|
945
|
+
| `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). |
|
|
946
|
+
| `NEDBD_CAST_MODEL` | unset | Explicit path to a `model.cast` container. Otherwise searched in the data dir, `$CAST_HOME`, and `~/.cache/nedb-cast-slm/`. |
|
|
911
947
|
|
|
912
|
-
|
|
913
|
-
#
|
|
914
|
-
|
|
915
|
-
|
|
948
|
+
```bash
|
|
949
|
+
# Create a database with seed data and relations
|
|
950
|
+
curl -X POST :7070/v1/databases -d '{
|
|
951
|
+
"name": "shop",
|
|
952
|
+
"init": {
|
|
953
|
+
"indexes": [["users","status","eq"]],
|
|
954
|
+
"seed": {"users": [{"_id":"u1","name":"Alice","status":"active"}]},
|
|
955
|
+
"links": [["users:u1","buys","orders:o1"]]
|
|
956
|
+
}}'
|
|
916
957
|
|
|
917
|
-
#
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
if_seq=d.get("_seq", -1))])
|
|
922
|
-
c.cas_retry(bump)
|
|
958
|
+
# Query — the endpoint speaks neSQL: SQL *or* NQL, routed on the first keyword
|
|
959
|
+
curl -X POST :7070/v1/databases/shop/query \
|
|
960
|
+
-d '{"nql":"SELECT name FROM users WHERE status = '"'"'active'"'"' ORDER BY name"}'
|
|
961
|
+
# → {"rows":[{"name":"Alice"}],"count":1,"dialect":"sql", ...}
|
|
923
962
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
```
|
|
963
|
+
curl -X POST :7070/v1/databases/shop/query \
|
|
964
|
+
-d '{"nql":"FROM users WHERE status = \"active\" ORDER BY name ASC"}'
|
|
965
|
+
# → {"rows":[...],"count":1,"dialect":"nql", ...}
|
|
928
966
|
|
|
929
|
-
A CAS miss raises the **same `PreconditionFailed`** (with the same
|
|
930
|
-
`.failures` shape) the embedded engine raises — code written against
|
|
931
|
-
`NEDB.tx` ports to the HTTP client without changing its except-clauses.
|
|
932
|
-
Typed errors throughout: `NedbAuthError`, `NedbNotFound`, `NedbBadRequest`,
|
|
933
|
-
`NedbConflict`, `CasExhausted`.
|
|
934
967
|
|
|
935
|
-
|
|
968
|
+
**The field is still called `nql`, and its contents no longer have to be.** This
|
|
969
|
+
endpoint accepts **neSQL** — NQL *or* PostgreSQL SQL — and answers with the
|
|
970
|
+
`dialect` it chose. The name is unchanged because every existing HTTP client
|
|
971
|
+
sends it; renaming would break them to gain nothing. Old NQL clients are
|
|
972
|
+
unaffected.
|
|
936
973
|
|
|
937
|
-
|
|
974
|
+
Routing is **structural, not guessed**. NQL statements begin `FROM`; PostgreSQL
|
|
975
|
+
has no statement form that begins with `FROM`, so the leading keyword partitions
|
|
976
|
+
the two vocabularies rather than hinting at them. A first word in neither is
|
|
977
|
+
refused *naming both* — never handed to whichever parser seems likelier.
|
|
938
978
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
r = wrap_redis(redis.Redis("localhost", 6379), db_name="rideshare")
|
|
946
|
-
|
|
947
|
-
# Step 1 — register: map Redis key globs to NEDB collections (chainable)
|
|
948
|
-
(r.nedb
|
|
949
|
-
.register("driver:*", collection="driver", value_parser=json.loads)
|
|
950
|
-
.register("trip:*", collection="trip", value_type="hash")
|
|
951
|
-
)
|
|
952
|
-
|
|
953
|
-
# Step 2 — backfill: import all existing Redis data into NEDB in one pass
|
|
954
|
-
imported = r.nedb.backfill() # → int (keys imported)
|
|
955
|
-
|
|
956
|
-
# Step 3 — shadow: all future r.set/hset/... auto-chain into NEDB
|
|
957
|
-
r.nedb.shadow_writes = True
|
|
958
|
-
|
|
959
|
-
# ─── Alice's app keeps running — zero changes ───────────────────────────
|
|
960
|
-
r.set("driver:d1", json.dumps({"name": "Bob", "status": "active"})) # ← shadowed
|
|
961
|
-
r.hset("trip:t1", mapping={"status": "en_route", "driver_id": "d1"}) # ← shadowed
|
|
962
|
-
|
|
963
|
-
# ─── New features available on the same connection ──────────────────────
|
|
964
|
-
r.nedb.query('FROM driver WHERE status = "active" ORDER BY lat ASC')
|
|
965
|
-
r.nedb.verify() # → True (every write chain-verified)
|
|
966
|
-
r.nedb.head() # → 64-char BLAKE2b commitment hash
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
**Isolation guarantee:** NEDB never writes to Alice's namespace. It owns only:
|
|
970
|
-
|
|
971
|
-
| Key | Type | Purpose |
|
|
972
|
-
|-----|------|---------|
|
|
973
|
-
| `nedb:{db_name}:oplog` | Redis Stream | append-only op log |
|
|
974
|
-
| `nedb:{db_name}:snapshot` | Redis Hash | checkpoint |
|
|
975
|
-
| `nedb:{db_name}:meta` | Redis Hash | index config |
|
|
976
|
-
|
|
977
|
-
See [`examples/fakeredis_demo.py`](examples/fakeredis_demo.py) for a full local demo (no Redis server needed).
|
|
978
|
-
|
|
979
|
-
---
|
|
980
|
-
|
|
981
|
-
## Node.js
|
|
982
|
-
|
|
983
|
-
```javascript
|
|
984
|
-
import { NedbCore } from "nedb-engine";
|
|
985
|
-
|
|
986
|
-
const db = new NedbCore(); // in-memory
|
|
987
|
-
// const db = NedbCore.open("./data"); // durable
|
|
988
|
-
|
|
989
|
-
db.createIndex("users", "status", "eq");
|
|
990
|
-
db.put("users", "alice", JSON.stringify({ name: "Alice", age: 31, status: "active" }));
|
|
991
|
-
|
|
992
|
-
// Time-travel
|
|
993
|
-
const snap = db.seq(); // BigInt
|
|
994
|
-
db.put("users", "alice", JSON.stringify({ name: "Alice", age: 32, status: "retired" }));
|
|
995
|
-
JSON.parse(db.getAsOf("users", "alice", snap)).age; // → 31
|
|
996
|
-
|
|
997
|
-
// Full NQL
|
|
998
|
-
const rows = db.query('FROM users WHERE status = "active" ORDER BY age ASC');
|
|
999
|
-
rows.map(r => JSON.parse(r));
|
|
1000
|
-
|
|
1001
|
-
// Tamper evidence
|
|
1002
|
-
db.verify(); // → true
|
|
1003
|
-
db.head(); // → 64-char BLAKE2b commitment hash
|
|
1004
|
-
db.seq(); // → BigInt
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
---
|
|
1008
|
-
|
|
1009
|
-
## nedbd — the concurrent server daemon
|
|
1010
|
-
|
|
1011
|
-
nedbd runs NEDB as a long-lived process with an HTTP/JSON API and an optional RESP2 wire protocol. Built on a **single-writer group-commit sequencer** — parallel reads, batched durable writes, one hash-chain per database, zero write-write races.
|
|
1012
|
-
|
|
1013
|
-
```bash
|
|
1014
|
-
nedbd # :7070, data ./nedb-data (v1 AOF engine)
|
|
1015
|
-
nedbd --dag --data ./data # v2 DAG engine (or NEDBD_DAG=1)
|
|
1016
|
-
NEDBD_RESP2_PORT=6380 nedbd # also speak RESP2 (redis-cli compatible)
|
|
1017
|
-
nedbd --log-level 2 # 0=errors 1=requests 2=deploy 3=verbose
|
|
1018
|
-
|
|
1019
|
-
# Live event stream (since 2.2.31) — SSE: scan progress, ready, per-write head
|
|
1020
|
-
curl http://127.0.0.1:7070/events
|
|
1021
|
-
```
|
|
1022
|
-
|
|
1023
|
-
### Companion CLIs
|
|
1024
|
-
|
|
1025
|
-
Alongside the daemon, `cargo install nedb-engine` ships **`nedb-cli`** — operate on a store directory offline (`head`/`status`/`verify`/`get`/`scan`/`flush`/`repair`/`export`) — and **`nedb-inspector`**, a deterministic checker that warns when a durable open lacks flush-on-exit wiring. Full reference: [**docs/CLI.md**](docs/CLI.md).
|
|
1026
|
-
|
|
1027
|
-
### Startup modes
|
|
1028
|
-
|
|
1029
|
-
- **Warm start** — every restart after the first open reads the `MANIFEST` file and restores `seq` + Merkle `head` in **O(1)**. No scan, no replay, independent of dataset size. Boots in milliseconds.
|
|
1030
|
-
- **Cold start** — first open of an existing dataset spawns the integrity scan in a background thread *and accepts connections immediately*. Reads serve instantly from the content-addressed DAG; writes return `HTTP 503 startup in progress` until the `startup_ready` gate flips. Progress (objects, rate, ETA) streams over `GET /events`.
|
|
1031
|
-
|
|
1032
|
-
### Environment variables
|
|
1033
|
-
|
|
1034
|
-
| Variable | Default | Description |
|
|
1035
|
-
|---|---|---|
|
|
1036
|
-
| `NEDBD_DAG` | `0` | Set `1` to launch the v2 DAG engine (`nedbd-v2`). Same as `--dag`. |
|
|
1037
|
-
| `NEDBD_HOST` | `127.0.0.1` | Bind address. **v2.2.31** defaults to loopback (was `0.0.0.0`) — security hardening fix. Set explicitly to `0.0.0.0` to expose. |
|
|
1038
|
-
| `NEDBD_PORT` | `7070` | HTTP bind port. |
|
|
1039
|
-
| `NEDBD_TOKEN` | unset | Optional bearer token; required on every `/v1/*` request when set. |
|
|
1040
|
-
| `NEDB_TMK` | unset | 32-byte hex AES-256-GCM at-rest encryption key. |
|
|
1041
|
-
| `NEDBD_DATA` | `./nedb-data` | Root directory. v2 creates `dag/`, IdIndex sharded across **256 subdirectories**, and a small `MANIFEST` file. |
|
|
1042
|
-
| `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). |
|
|
1043
|
-
| `NEDBD_CAST_MODEL` | unset | Explicit path to a `model.cast` container. Otherwise searched in the data dir, `$CAST_HOME`, and `~/.cache/nedb-cast-slm/`. |
|
|
1044
|
-
|
|
1045
|
-
```bash
|
|
1046
|
-
# Create a database with seed data and relations
|
|
1047
|
-
curl -X POST :7070/v1/databases -d '{
|
|
1048
|
-
"name": "shop",
|
|
1049
|
-
"init": {
|
|
1050
|
-
"indexes": [["users","status","eq"]],
|
|
1051
|
-
"seed": {"users": [{"_id":"u1","name":"Alice","status":"active"}]},
|
|
1052
|
-
"links": [["users:u1","buys","orders:o1"]]
|
|
1053
|
-
}}'
|
|
1054
|
-
|
|
1055
|
-
# Query — the endpoint speaks neSQL: SQL *or* NQL, routed on the first keyword
|
|
1056
|
-
curl -X POST :7070/v1/databases/shop/query \
|
|
1057
|
-
-d '{"nql":"SELECT name FROM users WHERE status = '"'"'active'"'"' ORDER BY name"}'
|
|
1058
|
-
# → {"rows":[{"name":"Alice"}],"count":1,"dialect":"sql", ...}
|
|
1059
|
-
|
|
1060
|
-
curl -X POST :7070/v1/databases/shop/query \
|
|
1061
|
-
-d '{"nql":"FROM users WHERE status = \"active\" ORDER BY name ASC"}'
|
|
1062
|
-
# → {"rows":[...],"count":1,"dialect":"nql", ...}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
**The field is still called `nql`, and its contents no longer have to be.** This
|
|
1066
|
-
endpoint accepts **neSQL** — NQL *or* PostgreSQL SQL — and answers with the
|
|
1067
|
-
`dialect` it chose. The name is unchanged because every existing HTTP client
|
|
1068
|
-
sends it; renaming would break them to gain nothing. Old NQL clients are
|
|
1069
|
-
unaffected.
|
|
1070
|
-
|
|
1071
|
-
Routing is **structural, not guessed**. NQL statements begin `FROM`; PostgreSQL
|
|
1072
|
-
has no statement form that begins with `FROM`, so the leading keyword partitions
|
|
1073
|
-
the two vocabularies rather than hinting at them. A first word in neither is
|
|
1074
|
-
refused *naming both* — never handed to whichever parser seems likelier.
|
|
1075
|
-
|
|
1076
|
-
```bash
|
|
1077
|
-
curl -X POST :7070/v1/databases/shop/query -d '{"nql":"GRANT ALL ON users"}'
|
|
1078
|
-
# → 400 "GRANT" does not begin a statement in either half of neSQL
|
|
1079
|
-
# NQL statements begin with: FROM
|
|
1080
|
-
# SQL statements begin with: SELECT, INSERT, UPDATE, ...
|
|
1081
|
-
```
|
|
979
|
+
```bash
|
|
980
|
+
curl -X POST :7070/v1/databases/shop/query -d '{"nql":"GRANT ALL ON users"}'
|
|
981
|
+
# → 400 "GRANT" does not begin a statement in either half of neSQL
|
|
982
|
+
# NQL statements begin with: FROM
|
|
983
|
+
# SQL statements begin with: SELECT, INSERT, UPDATE, ...
|
|
984
|
+
```
|
|
1082
985
|
|
|
1083
986
|
It is the **same router** `nesql query` uses — `nedb_engine::neql::route`, which
|
|
1084
987
|
the CLI re-exports rather than copies. Two implementations of that decision
|
|
@@ -1182,14 +1085,13 @@ db.query("FROM blocks WHERE height BETWEEN 600000 AND 600100")
|
|
|
1182
1085
|
```
|
|
1183
1086
|
|
|
1184
1087
|
Measured on 20,000 rows with `scripts/bench_index_range.py` — two identical
|
|
1185
|
-
databases, one indexed, one not
|
|
1088
|
+
databases, one indexed, one not (the full table lives in
|
|
1089
|
+
[**Performance**](#performance--every-number-dated-and-reproducible)):
|
|
1186
1090
|
|
|
1187
1091
|
| Query | Scan | Indexed | Speedup |
|
|
1188
1092
|
| --- | --- | --- | --- |
|
|
1189
1093
|
| `WHERE fee = 10000` | 137 ms | 0.01 ms | 17,000× |
|
|
1190
|
-
| `WHERE fee IN (a, b, c)` | 185 ms | 0.02 ms | 9,700× |
|
|
1191
1094
|
| `WHERE fee BETWEEN …` (1% of rows) | 186 ms | 1.1 ms | 170× |
|
|
1192
|
-
| `WHERE fee BETWEEN …` (10% of rows) | 188 ms | 13 ms | 14× |
|
|
1193
1095
|
| unindexed field (control) | 188 ms | 187 ms | 1.0× |
|
|
1194
1096
|
|
|
1195
1097
|
The planner asks the index how many rows each candidate range covers and takes
|
|
@@ -1510,108 +1412,237 @@ Two habits that avoid most misses:
|
|
|
1510
1412
|
|
|
1511
1413
|
---
|
|
1512
1414
|
|
|
1513
|
-
##
|
|
1415
|
+
## Architecture
|
|
1514
1416
|
|
|
1515
|
-
|
|
1417
|
+
```
|
|
1418
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1419
|
+
put/del → │ OpLog (BLAKE2b hash chain · per-client nonce · │ ← single source of truth
|
|
1420
|
+
link │ idempotency keys · causal provenance fields) │
|
|
1421
|
+
└───────────────┬──────────────────────────────────────────┘
|
|
1422
|
+
deterministic fold │ (state = pure function of the log)
|
|
1423
|
+
┌──────────────┬──────────┴──────┬───────────────┬────────────────┐
|
|
1424
|
+
▼ ▼ ▼ ▼ ▼
|
|
1425
|
+
MVCC store Relations Indexes CauseMap BlobStore
|
|
1426
|
+
(time-travel) (graph+AS OF) eq/ord/search (reverse index) (Cascade CDC)
|
|
1516
1427
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
| Batch writes (500 ops/req) | **1,104 ops/s** | 0.9 ms | 1.2 ms |
|
|
1523
|
-
| Tamper-verify (30k objects) | ~21,000 BLAKE2b/sec | — | 1.38 s total |
|
|
1428
|
+
┌─────────────────────────────────┐
|
|
1429
|
+
Thread-safe → │ Sequencer (group-commit) │ ← single writer, parallel readers
|
|
1430
|
+
│ — one committer thread/db │
|
|
1431
|
+
│ — batch fsync │
|
|
1432
|
+
└─────────────────────────────────┘
|
|
1524
1433
|
|
|
1525
|
-
|
|
1434
|
+
Compatibility adapters: SQL · Redis · MongoDB
|
|
1435
|
+
Wire protocols: HTTP/JSON · RESP2 · PostgreSQL wire
|
|
1436
|
+
Encryption: AES-256-GCM at-rest (TMK/DEK double-envelope)
|
|
1437
|
+
```
|
|
1526
1438
|
|
|
1527
|
-
|
|
1439
|
+
The provenance machinery is load-bearing infrastructure, not metadata stapled on: `CauseMap` is a
|
|
1440
|
+
reverse index over causal edges (that is why `TRACE … REVERSE` is as fast as forward), and the
|
|
1441
|
+
`Relations` layer backs both `TRAVERSE` and the `AS OF` joins.
|
|
1528
1442
|
|
|
1529
|
-
|
|
1530
|
-
|---|---|---|
|
|
1531
|
-
| Sequential PUT | ~23/s | 44 ms |
|
|
1532
|
-
| Concurrent PUT (16 workers) | ~92/s | 48 ms |
|
|
1533
|
-
| Batch PUT (500 ops/request) | ~520 ops/s | 1.9 ms/op |
|
|
1534
|
-
| Point-lookup read (NQL) | ~23/s | 44 ms |
|
|
1535
|
-
| Rust napi PUT (FFI) | ~70K/s | — |
|
|
1536
|
-
| Rust napi GET (FFI) | ~330K/s | — |
|
|
1443
|
+
---
|
|
1537
1444
|
|
|
1538
|
-
|
|
1445
|
+
## Install
|
|
1539
1446
|
|
|
1540
1447
|
```bash
|
|
1541
|
-
|
|
1542
|
-
|
|
1448
|
+
pip install nedb-engine # Python ≥ 3.8 — pure-Python + optional Rust native wheel
|
|
1449
|
+
npm install nedb-engine # Node ≥ 16 — napi-rs prebuilt binaries
|
|
1543
1450
|
```
|
|
1544
1451
|
|
|
1452
|
+
### Prebuilt platforms
|
|
1453
|
+
|
|
1454
|
+
Both registries ship prebuilt binaries for:
|
|
1455
|
+
|
|
1456
|
+
| Platform | libc | Python wheel | Node addon |
|
|
1457
|
+
|---|---|---|---|
|
|
1458
|
+
| Linux x86_64 | glibc | ✅ manylinux | ✅ |
|
|
1459
|
+
| Linux x86_64 | musl (Alpine) | ✅ musllinux | ✅ |
|
|
1460
|
+
| Linux aarch64 (Graviton, Ampere, Apple-Silicon containers) | glibc | ✅ manylinux | ✅ |
|
|
1461
|
+
| Linux aarch64 | musl (Alpine) | ✅ musllinux | ✅ |
|
|
1462
|
+
| macOS arm64 + x86_64 | — | ✅ | ✅ |
|
|
1463
|
+
| Windows x86_64 | MSVC | ✅ | ✅ |
|
|
1464
|
+
|
|
1465
|
+
On Python, any platform without a prebuilt wheel still installs: pip falls back
|
|
1466
|
+
to the universal `py3-none-any` wheel and you get the pure-Python v1 AOF engine
|
|
1467
|
+
(correct, slower, no embedded DAG). On Node there is no such fallback — an
|
|
1468
|
+
unlisted platform has no addon.
|
|
1469
|
+
|
|
1545
1470
|
---
|
|
1546
1471
|
|
|
1547
|
-
##
|
|
1472
|
+
## Python — 5-minute tour
|
|
1548
1473
|
|
|
1549
|
-
|
|
1474
|
+
Every example in this tour was executed against the released **8.0.0** package.
|
|
1550
1475
|
|
|
1551
|
-
|
|
1476
|
+
```python
|
|
1477
|
+
from nedb import NEDB
|
|
1552
1478
|
|
|
1553
|
-
|
|
1479
|
+
db = NEDB("./mydata") # durable: every op is AOF-logged, fsync'd, and hash-chained
|
|
1480
|
+
# db = NEDB() # or in-memory
|
|
1554
1481
|
|
|
1555
|
-
|
|
1482
|
+
db.create_index("users", "status", "eq")
|
|
1483
|
+
db.create_index("users", "bio", "search")
|
|
1556
1484
|
|
|
1557
|
-
|
|
1485
|
+
db.put("users", "alice", {"name": "Alice", "age": 31, "status": "active", "bio": "rust hacker"})
|
|
1486
|
+
db.put("users", "bob", {"name": "Bob", "age": 24, "status": "active", "bio": "python dev"})
|
|
1558
1487
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1488
|
+
# NQL: WHERE + ORDER BY + LIMIT + SEARCH + TRAVERSE + GROUP BY
|
|
1489
|
+
db.query('FROM users WHERE status = "active" ORDER BY age ASC')
|
|
1490
|
+
db.query('FROM users SEARCH "rust"')
|
|
1491
|
+
db.query('FROM users GROUP BY status COUNT')
|
|
1563
1492
|
|
|
1564
|
-
|
|
1493
|
+
# Full boolean predicates — IN, BETWEEN, LIKE, IS NULL, OR, NOT, parentheses
|
|
1494
|
+
db.query('FROM users WHERE status IN ("active", "trialing")')
|
|
1495
|
+
db.query('FROM users WHERE age BETWEEN 25 AND 40')
|
|
1496
|
+
db.query('FROM users WHERE bio LIKE "%rust%" AND NOT (status = "retired")')
|
|
1497
|
+
db.query('FROM users WHERE (age < 25 OR age > 60) AND bio IS NOT NULL')
|
|
1565
1498
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1499
|
+
# Time-travel — AS OF any past sequence
|
|
1500
|
+
snap = db.seq
|
|
1501
|
+
db.put("users", "alice", {"name": "Alice", "age": 32, "status": "retired"})
|
|
1502
|
+
db.get("users", "alice", as_of=snap) # → age 31, status active
|
|
1569
1503
|
|
|
1570
|
-
#
|
|
1571
|
-
|
|
1504
|
+
# Bi-temporal — VALID AS OF any past date
|
|
1505
|
+
db.put("policy", "rate_2024", {"pct": 5.0}, valid_from="2024-01-01", valid_to="2024-12-31")
|
|
1506
|
+
db.put("policy", "rate_2025", {"pct": 6.0}, valid_from="2025-01-01")
|
|
1507
|
+
db.query('FROM policy VALID AS OF "2024-06-15"') # → rate 5.0
|
|
1508
|
+
|
|
1509
|
+
# Causal Write Provenance — why did this write happen?
|
|
1510
|
+
db.put("inputs", "msg_1", {"text": "user prefers dark mode"})
|
|
1511
|
+
seq_msg = db.seq
|
|
1512
|
+
db.put("beliefs", "dark_mode", {"value": True},
|
|
1513
|
+
caused_by=[seq_msg], evidence="user_message", confidence=0.95)
|
|
1514
|
+
db.query('FROM beliefs WHERE _id = "dark_mode" TRACE caused_by') # → msg_1
|
|
1515
|
+
db.query('FROM inputs WHERE _id = "msg_1" TRACE caused_by REVERSE') # → dark_mode
|
|
1516
|
+
|
|
1517
|
+
# Relations + graph traversal
|
|
1518
|
+
db.link("users:alice", "follows", "users:bob")
|
|
1519
|
+
db.query('FROM users WHERE _id = "alice" TRAVERSE follows')
|
|
1520
|
+
|
|
1521
|
+
# Hash-chain integrity — verify() is a call, head is the commitment
|
|
1522
|
+
assert db.verify() # cryptographic proof — no tampering
|
|
1523
|
+
db.head # → 64-char BLAKE2b Merkle head (a property, not a call)
|
|
1524
|
+
|
|
1525
|
+
# SQL, Redis, MongoDB compatibility adapters
|
|
1526
|
+
from nedb import sql_exec, RedisCompat, MongoClient
|
|
1527
|
+
sql_exec(db, "SELECT * FROM users WHERE status = 'active' ORDER BY age DESC")
|
|
1528
|
+
r = RedisCompat(db); r.execute("HSET", "user:1", "name", "Alice")
|
|
1529
|
+
MongoClient(db)["users"].find({"status": "active"}).sort("age", -1).to_list()
|
|
1572
1530
|
```
|
|
1573
1531
|
|
|
1574
|
-
|
|
1532
|
+
---
|
|
1575
1533
|
|
|
1576
|
-
|
|
1534
|
+
## Official Python client — talk to nedbd over HTTP
|
|
1577
1535
|
|
|
1578
|
-
|
|
1536
|
+
Running the daemon? `nedb.client.NedbClient` is the official client for its
|
|
1537
|
+
HTTP API — extracted from the battle-tested clients that ran a production
|
|
1538
|
+
Redis→NEDB mainnet migration, speaking the full route surface: queries,
|
|
1539
|
+
atomic CAS transactions, TTL, indexes, relations, Merkle proofs, and the
|
|
1540
|
+
Mongo-compat endpoint. Env-var defaults (`NEDBD_URL`, `NEDBD_TOKEN`,
|
|
1541
|
+
`NEDB_DB`) mirror the daemon's own.
|
|
1579
1542
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
| 2,002 coins / 275 kB | **1.93 s** | *minutes* |
|
|
1583
|
-
| 2,549 coins / 366 kB | **1.71 s** | *minutes* |
|
|
1543
|
+
```python
|
|
1544
|
+
from nedb import NedbClient, PreconditionFailed, op_put
|
|
1584
1545
|
|
|
1585
|
-
|
|
1546
|
+
c = NedbClient("http://127.0.0.1:7070", db="app", token="s3cret")
|
|
1547
|
+
c.ensure_database()
|
|
1586
1548
|
|
|
1587
|
-
|
|
1549
|
+
c.put("users", "u1", {"id": "u1", "email": "a@b.c"}, idem="signup-u1")
|
|
1550
|
+
c.query('FROM users WHERE email = "a@b.c"') # full NQL rides through
|
|
1551
|
+
c.query("FROM users AS OF 41") # time-travel included
|
|
1588
1552
|
|
|
1589
|
-
|
|
1553
|
+
# Atomic all-or-nothing transaction with engine-checked preconditions —
|
|
1554
|
+
# the primitive that replaces Redis Lua scripts (if_seq: N = CAS, -1 = create-once)
|
|
1555
|
+
doc = c.get_doc("users", "u1") # docs carry _seq
|
|
1556
|
+
c.tx([op_put("users", "u1", {**doc, "plan": "pro"}, if_seq=doc["_seq"])])
|
|
1557
|
+
|
|
1558
|
+
# Contested writes: retry ONLY on PreconditionFailed, capped backoff
|
|
1559
|
+
def bump():
|
|
1560
|
+
d = c.get_doc("counters", "hits") or {"n": 0}
|
|
1561
|
+
return c.tx([op_put("counters", "hits", {"n": d.get("n", 0) + 1},
|
|
1562
|
+
if_seq=d.get("_seq", -1))])
|
|
1563
|
+
c.cas_retry(bump)
|
|
1564
|
+
|
|
1565
|
+
# Integrity, verifiable WITHOUT trusting the server
|
|
1566
|
+
proof = c.proof(c.log(limit=1)[0]["hash"])
|
|
1567
|
+
from nedb import verify_proof; verify_proof(proof) # -> True, locally
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
A CAS miss raises the **same `PreconditionFailed`** (with the same
|
|
1571
|
+
`.failures` shape) the embedded engine raises — code written against
|
|
1572
|
+
`NEDB.tx` ports to the HTTP client without changing its except-clauses.
|
|
1573
|
+
Typed errors throughout: `NedbAuthError`, `NedbNotFound`, `NedbBadRequest`,
|
|
1574
|
+
`NedbConflict`, `CasExhausted`.
|
|
1590
1575
|
|
|
1591
1576
|
---
|
|
1592
1577
|
|
|
1593
|
-
##
|
|
1578
|
+
## Redis layer-2 — wrap_redis()
|
|
1579
|
+
|
|
1580
|
+
Already running on Redis? Wrap your connection in one line and gain NEDB features *alongside* your existing Redis app — no migration required.
|
|
1581
|
+
|
|
1582
|
+
```python
|
|
1583
|
+
import redis, json
|
|
1584
|
+
from nedb import wrap_redis
|
|
1585
|
+
|
|
1586
|
+
r = wrap_redis(redis.Redis("localhost", 6379), db_name="rideshare")
|
|
1587
|
+
|
|
1588
|
+
# Step 1 — register: map Redis key globs to NEDB collections (chainable)
|
|
1589
|
+
(r.nedb
|
|
1590
|
+
.register("driver:*", collection="driver", value_parser=json.loads)
|
|
1591
|
+
.register("trip:*", collection="trip", value_type="hash")
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
# Step 2 — backfill: import all existing Redis data into NEDB in one pass
|
|
1595
|
+
imported = r.nedb.backfill() # → int (keys imported)
|
|
1596
|
+
|
|
1597
|
+
# Step 3 — shadow: all future r.set/hset/... auto-chain into NEDB
|
|
1598
|
+
r.nedb.shadow_writes = True
|
|
1599
|
+
|
|
1600
|
+
# ─── Alice's app keeps running — zero changes ───────────────────────────
|
|
1601
|
+
r.set("driver:d1", json.dumps({"name": "Bob", "status": "active"})) # ← shadowed
|
|
1602
|
+
r.hset("trip:t1", mapping={"status": "en_route", "driver_id": "d1"}) # ← shadowed
|
|
1594
1603
|
|
|
1604
|
+
# ─── New features available on the same connection ──────────────────────
|
|
1605
|
+
r.nedb.query('FROM driver WHERE status = "active" ORDER BY lat ASC')
|
|
1606
|
+
r.nedb.verify() # → True (every write chain-verified)
|
|
1607
|
+
r.nedb.head() # → 64-char BLAKE2b commitment hash
|
|
1595
1608
|
```
|
|
1596
|
-
┌──────────────────────────────────────────────────────────┐
|
|
1597
|
-
put/del → │ OpLog (BLAKE2b hash chain · per-client nonce · │ ← single source of truth
|
|
1598
|
-
link │ idempotency keys · causal provenance fields) │
|
|
1599
|
-
└───────────────┬──────────────────────────────────────────┘
|
|
1600
|
-
deterministic fold │ (state = pure function of the log)
|
|
1601
|
-
┌──────────────┬──────────┴──────┬───────────────┬────────────────┐
|
|
1602
|
-
▼ ▼ ▼ ▼ ▼
|
|
1603
|
-
MVCC store Relations Indexes CauseMap BlobStore
|
|
1604
|
-
(time-travel) (graph+AS OF) eq/ord/search (reverse index) (Cascade CDC)
|
|
1605
1609
|
|
|
1606
|
-
|
|
1607
|
-
Thread-safe → │ Sequencer (group-commit) │ ← single writer, parallel readers
|
|
1608
|
-
│ — one committer thread/db │
|
|
1609
|
-
│ — batch fsync │
|
|
1610
|
-
└─────────────────────────────────┘
|
|
1610
|
+
**Isolation guarantee:** NEDB never writes to Alice's namespace. It owns only:
|
|
1611
1611
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1612
|
+
| Key | Type | Purpose |
|
|
1613
|
+
|-----|------|---------|
|
|
1614
|
+
| `nedb:{db_name}:oplog` | Redis Stream | append-only op log |
|
|
1615
|
+
| `nedb:{db_name}:snapshot` | Redis Hash | checkpoint |
|
|
1616
|
+
| `nedb:{db_name}:meta` | Redis Hash | index config |
|
|
1617
|
+
|
|
1618
|
+
See [`examples/fakeredis_demo.py`](examples/fakeredis_demo.py) for a full local demo (no Redis server needed).
|
|
1619
|
+
|
|
1620
|
+
---
|
|
1621
|
+
|
|
1622
|
+
## Node.js
|
|
1623
|
+
|
|
1624
|
+
```javascript
|
|
1625
|
+
import { NedbCore } from "nedb-engine";
|
|
1626
|
+
|
|
1627
|
+
const db = new NedbCore(); // in-memory
|
|
1628
|
+
// const db = NedbCore.open("./data"); // durable
|
|
1629
|
+
|
|
1630
|
+
db.createIndex("users", "status", "eq");
|
|
1631
|
+
db.put("users", "alice", JSON.stringify({ name: "Alice", age: 31, status: "active" }));
|
|
1632
|
+
|
|
1633
|
+
// Time-travel
|
|
1634
|
+
const snap = db.seq(); // BigInt
|
|
1635
|
+
db.put("users", "alice", JSON.stringify({ name: "Alice", age: 32, status: "retired" }));
|
|
1636
|
+
JSON.parse(db.getAsOf("users", "alice", snap)).age; # → 31
|
|
1637
|
+
|
|
1638
|
+
// Full NQL
|
|
1639
|
+
const rows = db.query('FROM users WHERE status = "active" ORDER BY age ASC');
|
|
1640
|
+
rows.map(r => JSON.parse(r));
|
|
1641
|
+
|
|
1642
|
+
// Tamper evidence
|
|
1643
|
+
db.verify(); // → true
|
|
1644
|
+
db.head(); // → 64-char BLAKE2b commitment hash
|
|
1645
|
+
db.seq(); // → BigInt
|
|
1615
1646
|
```
|
|
1616
1647
|
|
|
1617
1648
|
---
|
|
@@ -1644,53 +1675,98 @@ const rows = await db.query("FROM blocks LIMIT 10");
|
|
|
1644
1675
|
|
|
1645
1676
|
---
|
|
1646
1677
|
|
|
1678
|
+
## NEDB v3 — Segment / Pack Object Store
|
|
1679
|
+
|
|
1680
|
+
**v3 is an opt-in storage substrate that replaces the loose one-file-per-object layout with append-only *segment packs* — the difference between a chainstate flush that takes *minutes* and one that takes *under two seconds*.** It is **off by default** (byte-for-byte v2), enabled with one flag, and **transparent** to everything above the storage layer: NQL, `AS OF`, `VALID AS OF`, `TRACE`, the BLAKE2b Merkle head, and causal provenance all behave identically.
|
|
1681
|
+
|
|
1682
|
+
### Why it exists
|
|
1683
|
+
|
|
1684
|
+
v2 stores every document version as its own content-addressed file at `objects/{hash[:2]}/{hash[2:]}`. That makes writes trivially atomic (write `.tmp` → `rename`) and corruption-proof — but each write costs a file create + `fsync` + rename **plus** a directory B-tree update. At scale that filesystem-metadata churn dominates: on a busy disk it caps sustained writes around **~185/s**, and a batch flush of a few thousand objects degrades into minutes. The bottleneck is the *number of files touched*, not the bytes written.
|
|
1685
|
+
|
|
1686
|
+
### What it does
|
|
1687
|
+
|
|
1688
|
+
v3 batches objects into append-only **segment packs** — `objects/segments/seg-NNNNNN.dat` — where each record is `[content_len: u32-LE][content]`. A write appends to the active segment and updates an in-memory `hash → (segment_id, offset, len)` map; a batch commits with a **single `fsync`**. Thousands of per-file syscalls collapse into one sequential append plus one durability point, so **flush cost scales with bytes (sequential I/O), not object-count × syscall overhead.**
|
|
1689
|
+
|
|
1690
|
+
- **Compaction / pruning** — `compact()` keeps the *live set* (the current version of every document, resolved from the id-index), rewrites those records into fresh segments, and reclaims the superseded/dead versions.
|
|
1691
|
+
- **`.idx` sidecars** — each segment carries a sidecar (`NIX1` magic + entry count + fixed 44-byte entries + a BLAKE2b-256 checksum) so reopen rebuilds the in-memory index by reading the sidecar instead of scanning the whole pack. A missing or corrupt sidecar falls back to a full scan-and-heal — slower, never fatal.
|
|
1692
|
+
- **Dual-read migration** — opening an existing v2 store in v3 mode is **non-destructive**: old loose objects stay fully readable, and only *new* writes go to segments. No migration step, no downtime, no rewrite.
|
|
1693
|
+
- **Durable flush-on-close** — `flush_all()` (and `Db`'s `Drop`) fsync the active segment, matching the flush-on-close contract of sled / RocksDB.
|
|
1694
|
+
|
|
1695
|
+
### How to enable
|
|
1696
|
+
|
|
1697
|
+
```bash
|
|
1698
|
+
# Engine / nedbd-v2 (the native daemon from npm / the native wheel)
|
|
1699
|
+
nedbd-v2 --dag-v3 --data /var/lib/nedb # real flag as of v2.4.3 — or set NEDB_DAG_V3=1
|
|
1700
|
+
|
|
1701
|
+
# itcd — Bitcoin-fork node embedding NEDB via nedb-ffi
|
|
1702
|
+
interchainedd -dagv3 # puts chainstate AND block index on segments
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
The switch is read once, when each database's object store is constructed at open time. Default off → v2 loose objects.
|
|
1706
|
+
|
|
1707
|
+
### Real-world result
|
|
1708
|
+
|
|
1709
|
+
itcd (a Bitcoin Core 0.21 fork that replaces LevelDB chainstate with NEDB) syncing on `-dagv3`, measured `FlushStateToDisk` on real chainstate:
|
|
1710
|
+
|
|
1711
|
+
| Flush (coins → disk) | v3 segment store | v2 loose store |
|
|
1712
|
+
|---|---|---|
|
|
1713
|
+
| 2,002 coins / 275 kB | **1.93 s** | *minutes* |
|
|
1714
|
+
| 2,549 coins / 366 kB | **1.71 s** | *minutes* |
|
|
1715
|
+
|
|
1716
|
+
Note the *larger* batch finishing *faster* — v3's cost is dominated by the single per-batch `fsync`, not per-coin work, so effective throughput (~1,000–1,500 coins/s here) climbs as batches grow, against the loose store's ~185 writes/s metadata ceiling. The gap only widens as the UTXO set grows: sequential-append cost tracks data volume, while per-file cost compounds with object count.
|
|
1717
|
+
|
|
1718
|
+
### When to use it
|
|
1719
|
+
|
|
1720
|
+
Reach for v3 on high-write, large-object-count workloads — blockchain chainstate / block index, event sourcing, high-frequency agent memory. For small or read-mostly stores the loose layout is perfectly fine, which is exactly why v3 stays opt-in.
|
|
1721
|
+
|
|
1722
|
+
---
|
|
1723
|
+
|
|
1647
1724
|
## Repo layout
|
|
1648
1725
|
|
|
1649
1726
|
```
|
|
1650
1727
|
python/nedb/ reference engine (pure Python — always-works baseline)
|
|
1651
1728
|
rust/
|
|
1729
|
+
nedb-v2/ v2 DAG engine (tokio + axum + BLAKE2b DAG) — the core everything binds
|
|
1730
|
+
nesql-cli/ the nesql CLI (query / root / inspect / diff / tag / branch / merge)
|
|
1652
1731
|
nedb-core/ v1 production Rust engine (shared by both runtimes)
|
|
1653
1732
|
nedb-py/ maturin PyO3 binding → PyPI native wheels
|
|
1654
1733
|
nedb-node/ napi-rs binding → npm native addons
|
|
1655
|
-
nedb-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1734
|
+
nedb-wrap/ the wrap_* surface at native speed
|
|
1735
|
+
vendor/postgresql/ PostgreSQL 17.4 grammar, vendored (gram.y, kwlist.h, system_views.sql, information_schema.sql)
|
|
1736
|
+
distributions/ crypto-database + aof-db submodules (the tri-distribution release)
|
|
1737
|
+
bench/ benchmarks.py + RESULTS.md — the dated embedded-core numbers
|
|
1659
1738
|
tests/ engine + concurrent + causal + bitemporal + deploy + perf benchmarks
|
|
1660
|
-
|
|
1661
|
-
docs/
|
|
1739
|
+
vectors/ state_root_v1.json — cross-engine state-root test vectors
|
|
1740
|
+
docs/ SPEC.md · CLI.md · DURABILITY.md · REPLICATION.md · BENCH-sqlselect.md
|
|
1741
|
+
client/ nedb-engine-client — async Python + TypeScript HTTP clients
|
|
1742
|
+
examples/ agent-loop · mini-chain · resp2 demos · fakeredis demo
|
|
1743
|
+
scripts/ release.py · test-cast.sh · bench_index_range.py · seed-shop.sh
|
|
1662
1744
|
```
|
|
1663
1745
|
|
|
1664
1746
|
---
|
|
1665
1747
|
|
|
1666
|
-
##
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
-
|
|
1672
|
-
|
|
1673
|
-
-
|
|
1674
|
-
|
|
1675
|
-
-
|
|
1676
|
-
-
|
|
1677
|
-
|
|
1678
|
-
-
|
|
1679
|
-
-
|
|
1680
|
-
|
|
1681
|
-
-
|
|
1682
|
-
-
|
|
1683
|
-
-
|
|
1684
|
-
-
|
|
1685
|
-
-
|
|
1686
|
-
-
|
|
1687
|
-
-
|
|
1688
|
-
- [ ] PyO3 + napi-rs bindings updated to v2 DAG API
|
|
1689
|
-
- [ ] NEDB Studio DAG mode toggle
|
|
1690
|
-
- [ ] Merkle inclusion proofs — prove a document existed at a specific time to a third party
|
|
1691
|
-
- [ ] Git-style branching — fork database state, experiment, merge or discard
|
|
1692
|
-
- [ ] Agent Memory SDK — `Memory.remember()` / `Memory.recall()` / `Memory.trace()`
|
|
1693
|
-
- [ ] Live query subscriptions (SSE) — push diffs when query results change
|
|
1748
|
+
## Ship log — the highlights, in order
|
|
1749
|
+
|
|
1750
|
+
NEDB's release history outgrew any changelog, and the README was where versions went to pile up.
|
|
1751
|
+
This is the map; the commits are the story.
|
|
1752
|
+
|
|
1753
|
+
- **8.x — neSQL ships.** PostgreSQL's real grammar vendored and extended; one evaluator answers every
|
|
1754
|
+
`SELECT` with nothing to enable; NQL's verbs are SQL clauses; the `nesql` CLI ships in the wheel.
|
|
1755
|
+
- **7.x — the neSQL endpoint surface.** The HTTP `/query` route speaks both dialects through one
|
|
1756
|
+
router; the CLI's publish path lands.
|
|
1757
|
+
- **6.x — hardening, discipline, the road to one name.**
|
|
1758
|
+
- **5.x — neSQL named, registered, vendored.** The grammar lands at `vendor/postgresql/`;
|
|
1759
|
+
`nesql` reserved on all three registries.
|
|
1760
|
+
- **4.0 — BUSL-1.1** (free under $1M revenue; Apache 2.0 on the 2030-09-11 Change Date).
|
|
1761
|
+
- **3.3 — the query language grows up.** Full boolean predicates in both engines; nine silent
|
|
1762
|
+
defects fixed; cross-engine parity gated.
|
|
1763
|
+
- **3.2 — the wrap family.** Redis/SQLite/Postgres/MySQL/Mongo adapters; durability fixes; PR CI.
|
|
1764
|
+
- **3.0–3.3.1 — the MIT era** (irrevocably MIT).
|
|
1765
|
+
- **2.8 — Cast.** A 3.33M-parameter model trained inside the engine's own parser; drift detection.
|
|
1766
|
+
- **2.5 — flush-on-exit, nedb-cli, inspector, replication contract.**
|
|
1767
|
+
- **2.4 — v3 segment store** (`--dag-v3`): itcd chainstate flush minutes → ~1.3 s.
|
|
1768
|
+
- **2.2 — v2 DAG engine.** Content-addressed Merkle DAG, O(1) warm start, SSE `/events`.
|
|
1769
|
+
- **1.x — v1 AOF engine.** Hash-chained log, MVCC time-travel, bi-temporal, `TRACE caused_by`.
|
|
1694
1770
|
|
|
1695
1771
|
---
|
|
1696
1772
|
|
|
@@ -1702,14 +1778,16 @@ Prompt-to-database scaffolding GUI with schema graph, NQL console, time-travel s
|
|
|
1702
1778
|
|
|
1703
1779
|
---
|
|
1704
1780
|
|
|
1705
|
-
## Repos
|
|
1781
|
+
## Repos & packages
|
|
1706
1782
|
|
|
1707
|
-
|
|
|
1783
|
+
| Where | What |
|
|
1708
1784
|
|---|---|
|
|
1709
|
-
| [
|
|
1785
|
+
| [Eth-Interchained/nedb](https://github.com/Eth-Interchained/nedb) | **canonical source** — engine, Rust core, CI, this README |
|
|
1786
|
+
| [aiassistsecure/nedb](https://github.com/aiassistsecure/nedb) | mirror + [GitHub Pages site](https://nedb.aiassist.net) |
|
|
1787
|
+
| [Eth-Interchained/neSQL](https://github.com/Eth-Interchained/neSQL) | the language — both halves of the grammar, CLI source |
|
|
1710
1788
|
| [aiassistsecure/nedb-studio](https://github.com/aiassistsecure/nedb-studio) | Studio UI (GPLv3) |
|
|
1711
1789
|
|
|
1712
|
-
**Packages:** [PyPI nedb-engine](https://pypi.org/project/nedb-engine/) · [npm nedb-engine](https://www.npmjs.com/package/nedb-engine)
|
|
1790
|
+
**Packages:** [PyPI nedb-engine](https://pypi.org/project/nedb-engine/) · [npm nedb-engine](https://www.npmjs.com/package/nedb-engine) · [crates.io nedb-engine](https://crates.io/crates/nedb-engine) — plus the aligned `crypto-database` and `aof-db` distributions on all three registries.
|
|
1713
1791
|
|
|
1714
1792
|
---
|
|
1715
1793
|
|
|
@@ -1737,7 +1815,11 @@ Requires `GITHUB_TOKEN` (`repo` + `workflow` scope) in the environment. It never
|
|
|
1737
1815
|
|
|
1738
1816
|
## License
|
|
1739
1817
|
|
|
1740
|
-
**
|
|
1818
|
+
**Business Source License 1.1** — free for any organisation under USD $1M annual revenue, including
|
|
1819
|
+
commercial and production use. Converts to **Apache 2.0** on **2030-09-11**, automatically and
|
|
1820
|
+
permanently. Versions 3.0.0–3.3.1 remain MIT, irrevocably. See [`LICENSE`](LICENSE) and
|
|
1821
|
+
[`COPYING-APACHE-2.0.txt`](COPYING-APACHE-2.0.txt).
|
|
1822
|
+
|
|
1741
1823
|
© 2026 INTERCHAINED LLC — [interchained.org](https://interchained.org)
|
|
1742
1824
|
|
|
1743
1825
|
---
|