nedb-engine 2.8.5 → 2.8.6

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 CHANGED
@@ -20,6 +20,83 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
20
20
 
21
21
  ---
22
22
 
23
+ ## ⚠️ New in 2.8.6 — Durability & Recovery (read this if you store anything you care about)
24
+
25
+ Three defects found by killing a real engine at every persistence boundary and by filling a real
26
+ filesystem to zero free blocks. All three are fixed. **If you are on 2.8.5 or earlier, upgrade.**
27
+
28
+ ### 1. A failed flush silently discarded acknowledged writes
29
+
30
+ `IdIndex::flush_write_buf` cleared every buffered entry regardless of whether its disk write
31
+ succeeded. So a flush that hit `ENOSPC` threw the entry away, and no later flush retried it.
32
+
33
+ Reproduced on a full 22 MiB filesystem: **30 rows acknowledged by `put() -> Ok`, then `list()`
34
+ returned 0 after reopen — while `verify()` reported all 30 objects healthy.** The content-addressed
35
+ objects were durable; the id-index entries that make them findable were gone.
36
+
37
+ ```
38
+ before: try_flush_all() -> (no return value) reopen -> 0 rows, verify() = 30 ok
39
+ after: try_flush_all() -> Err("id-index leaf rows/buf_25: No space left on device (os error 28)")
40
+ ...free space, retry -> Ok reopen -> 30 rows
41
+ ```
42
+
43
+ **Fixed:** an entry leaves the WAL only when its write actually landed. Failures stay buffered and
44
+ retry on the next flush.
45
+
46
+ ### 2. Flush errors were unobservable — new `try_flush_all()`
47
+
48
+ `flush_all()` returns `()` and logged fsync failures to stderr, so a caller could not tell a durable
49
+ flush from a failed one. Anything that takes a destructive or externally-visible action on the
50
+ strength of a persisted record needs to know.
51
+
52
+ ```rust
53
+ // Use this when the outcome matters:
54
+ db.try_flush_all()?; // Result<()> — id-index WAL + segment sync + MANIFEST
55
+
56
+ // Still available, still logs, nowhere to propagate (ticker / Drop):
57
+ db.flush_all();
58
+ ```
59
+
60
+ Also new: `Db::try_flush_manifest()` and `IdIndex::try_flush_write_buf()`.
61
+
62
+ ### 3. `repair` could not repair, and `since()` claimed "caught up" while behind
63
+
64
+ The cold scan rebuilt `seq_index`, per-collection tips, the Merkle head and `MANIFEST` — but **never
65
+ the id index**. A database whose WAL never reached disk came back with every object verifying and
66
+ `list()` empty, and `nedb-cli repair` printed success without fixing it, because
67
+ `start_cold_scan()` is a deliberate no-op on a warm store.
68
+
69
+ ```bash
70
+ nedb-cli repair ./data
71
+ # repaired: 203 id-index entr(ies) rebuilt, 203 node(s) verified, flushed
72
+ ```
73
+
74
+ ```rust
75
+ let restored = db.repair()?; // rebuild id index from objects; highest seq wins
76
+ ```
77
+
78
+ Every object carries its own `coll`, `id` and `seq`, so the id index is fully derivable — a lost WAL
79
+ is recoverable and nothing is invented. `repair()` also recomputes head and tips, so a repaired
80
+ database reopens **warm** instead of coming back up cold with an empty head.
81
+
82
+ Separately, `since()` set `has_more = hit_limit` alone. On a warm boot the seq index is empty **by
83
+ design** (that is why warm start is O(1)), so every lookup missed and `since()` returned zero nodes
84
+ with `has_more = false` — indistinguishable from genuinely up to date. A consumer following the
85
+ documented drain loop stopped one call in, with every record unread.
86
+
87
+ **Fixed:** `has_more` is true whenever the cursor is behind the log head. `ScanStatus` gains
88
+ **`seq_index_ready`** — replication consumers should gate on that, not on `scan_complete`, which is
89
+ true on a warm boot precisely because the scan was skipped.
90
+
91
+ ### Known sharp edge (documented, not changed)
92
+
93
+ `since()`'s cursor is **exclusive** and seqs start at 0, so `since(0, _)` returns `(0, head]` and the
94
+ very first write in a database (seq 0) is unreachable through any cursor value. Ten writes drain as
95
+ nine records. Changing the convention would break existing consumers; a replica seeded from
96
+ `since()` alone starts one record short.
97
+
98
+ ---
99
+
23
100
  ## NEDB v2.8.0 — Production Stable
24
101
 
25
102
  **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`.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nedb-engine",
3
- "version": "2.8.5",
3
+ "version": "2.8.6",
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",