|
1 | 1 | # @libredb/libredb |
2 | 2 |
|
| 3 | +## 0.2.0 |
| 4 | + |
| 5 | +### Minor Changes |
| 6 | + |
| 7 | +- 69f7161: Durability, safety, and API-contract hardening across the kernel, adapters, lenses, and CLI (the pre-announcement audit wave). |
| 8 | + |
| 9 | + On-disk format: new databases now begin with an 8-byte `LRDB` magic/version header, and each record header carries a checksum of its own length field. Files written by earlier releases (headerless) keep opening through a legacy read path, and keep their legacy record framing on later appends. The header is what lets `open()` refuse a file that is not a LibreDB database with a clear error instead of destroying it; the record-header checksum is what lets recovery refuse a damaged length field instead of mistaking it for a torn tail. |
| 10 | + |
| 11 | + DOWNGRADE WARNING: a file written by this release must never be opened by 0.1.3 or older — the old recovery cannot parse the header, classifies the whole file as a torn tail, and silently truncates it to zero bytes. Back up before any downgrade. Three smaller legacy-behavior changes: a headerless file whose only record is torn/incomplete now refuses to open as `NOT_A_DATABASE` (0.1.3 recovered it to an empty database; refusing is the safe reading, since such a file is indistinguishable from a foreign one); any file shorter than the 8-byte header is likewise refused untouched (a crash inside the first bytes of a brand-new database's first-ever commit therefore needs a manual delete — nothing in it was acknowledged); and a legacy length-field corruption still reads as a torn tail (the legacy format has no header checksum — the v1 format exists to close exactly that gap). |
| 12 | + |
| 13 | + Kernel: |
| 14 | + |
| 15 | + - `open({ path })` on a non-LibreDB file throws `NOT_A_DATABASE` and leaves the file byte-for-byte untouched (previously the file was silently truncated to zero). |
| 16 | + - Recovery classifies failures: a torn tail truncates (reported through the new `onRecovery` open option), while mid-log corruption throws `CORRUPT_WAL` and truncates nothing. Record payloads are structurally validated during replay. |
| 17 | + - A failed append/fsync latches the database: every later `transact()` throws `FAILED` until reopen, so an IO error can never lead recovery to silently drop later acknowledged commits. |
| 18 | + - `transact()` rejects async callbacks (`ASYNC_TRANSACTION`): writes after an `await` could never reach the log. |
| 19 | + - Keys and values are copied at the transaction boundary in both directions — caller buffer reuse and mutation of returned buffers can no longer corrupt the store. |
| 20 | + - `getRange` snapshots at first iteration, so delete-while-scanning visits every entry exactly once. |
| 21 | + - `close()` inside a transaction throws `CLOSE_IN_TRANSACTION` instead of surfacing a raw file error. |
| 22 | + - `open()` takes an exclusive per-file lock (`<path>.lock`, pid/host/nonce): a second writer throws `LOCKED` instead of silently diverging; locks from verifiably dead holders are reclaimed automatically. `FileSystem` gains an optional `lock()` seam method. |
| 23 | + - All kernel failures are now `LibreDbError` instances carrying a stable `code` (exported, with the `ErrorCode` and `RecoveryInfo` types). |
| 24 | + |
| 25 | + Adapters: |
| 26 | + |
| 27 | + - node-fs: creating a database fsyncs the parent directory (a fresh database can no longer vanish wholesale on power loss); a directory-fsync failure that is not a platform limitation (e.g. EIO) now surfaces as an error instead of being silently ignored; recovery truncation is fsync'd; reads are positional on the WAL's own file descriptor instead of re-reading the whole file per call. |
| 28 | + - OPFS: reads loop until filled, so a legal short read can no longer masquerade as a torn tail; recovery treats an incomplete read as an IO fault (`INCOMPLETE_READ`), never as license to truncate. |
| 29 | + |
| 30 | + Lenses: |
| 31 | + |
| 32 | + - Collection/table names may not be empty or contain `:` (both broke namespace isolation); ids keep full freedom. |
| 33 | + - Strings that are not well-formed UTF-16 (lone surrogates) are rejected wherever they would become keys, ids, names, or kv values — distinct strings can no longer silently collide on one key. |
| 34 | + - Relational `number` columns reject `NaN` and the infinities (JSON would store them as `null`). |
| 35 | + - `doc()` refuses a name cataloged as a relational table (it would bypass schema validation); `table()` refuses a document collection's name. |
| 36 | + - `find()`/`where()` reject a predicate field explicitly set to `undefined`, which previously matched documents _missing_ the field. |
| 37 | + |
| 38 | + CLI: |
| 39 | + |
| 40 | + - Write commands rely on the kernel's exclusive lock; `--force` removes a lock only when its holder is not verifiably alive, and never deletes a file that is not a libredb lock. Automatic reclaim is stricter still: only a lock whose holder is VERIFIABLY dead (same host, pid gone) is reclaimed without `--force` — anonymous locks (empty, or the sentinel-only 0.1.x format) carry no liveness information and now require `--force`. |
| 41 | + - `get`/`scan` escape control characters (including tab and newline) by default so untrusted values cannot inject terminal escape sequences — scripts that consumed values verbatim should pass `--raw`. |
| 42 | + |
| 43 | + New exports: `LibreDbError`, `ErrorCode`, `RecoveryInfo`, `nodeFileSystem`, and `readonlyFileSystem` (open a database for inspection with no lock and no writes — the supported way to read a file a live writer holds). |
| 44 | + |
| 45 | + Docker image now runs as a non-root user (distroless `:nonroot`, uid 65532): bind-mounted directories must be writable by that uid, or pass `--user "$(id -u):$(id -g)"`. |
| 46 | + |
3 | 47 | ## 0.1.3 |
4 | 48 |
|
5 | 49 | ### Patch Changes |
|
0 commit comments