@libredb/libredb 0.0.2 → 0.0.4

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
@@ -1,314 +1,211 @@
1
1
  # LibreDB
2
2
 
3
- > Status: pre-alpha. The kernel (`core.ts`) and all three lenses — `lens/kv.ts` (key-value), `lens/document.ts` (JSON documents), and `lens/relational.ts` (typed tables) — are implemented and tested.
3
+ **Multi-model without the magic. One core, three lenses, every line tested.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@libredb/libredb.svg)](https://www.npmjs.com/package/@libredb/libredb)
6
+ [![CI](https://github.com/libredb/libredb-database/actions/workflows/ci.yml/badge.svg)](https://github.com/libredb/libredb-database/actions/workflows/ci.yml)
7
+ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=libredb_libredb-database&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
8
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=libredb_libredb-database&metric=coverage)](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
9
+ [![license](https://img.shields.io/npm/l/@libredb/libredb.svg)](./LICENSE)
10
+ [![types: included](https://img.shields.io/badge/types-included-blue.svg)](https://www.typescriptlang.org/)
11
+ [![dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](./package.json)
12
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@libredb/libredb)](https://bundlephobia.com/package/@libredb/libredb)
13
+ [![status: pre-alpha](https://img.shields.io/badge/status-pre--alpha-orange.svg)](#project-status--roadmap)
14
+
15
+ LibreDB is a small, readable, embeddable, multi-model database written in TypeScript. It is built on
16
+ one idea: a database can be powerful and still be understood by opening its source. A single ordered
17
+ key-value core handles durability and transactions; key-value, document, and relational APIs are thin
18
+ *lenses* over that one core — not three separate engines. It runs in-memory for tests or file-backed
19
+ for durability, ships **zero runtime dependencies**, and proves its crash recovery with deterministic
20
+ simulation testing. Today it is pre-alpha, aimed at test and development environments — small enough to
21
+ learn how a database actually works, and serious enough to grow into more.
22
+
23
+ ## Highlights
24
+
25
+ - **One small core, three lenses** — key-value, document, and relational over a single ordered
26
+ key-value engine (FoundationDB-style), not three engines bolted together.
27
+ - **Multi-model** — raw strings, JSON documents, and schema-validated typed tables in the same
28
+ database, even the same file.
29
+ - **Readable by design** — the kernel is under 600 lines; open the source and learn how a database
30
+ actually works.
31
+ - **Embeddable, zero dependencies** — `bun add @libredb/libredb` and go; nothing else to install or
32
+ run.
33
+ - **In-memory or durable** — `open()` for tests, `open({ path })` for a crash-safe, WAL-backed,
34
+ fsync-on-commit file.
35
+ - **TypeScript-native** — full types shipped, ESM-only, tree-shakeable, under 4 kB min+brotli.
36
+ - **Crash recovery you can trust** — 100% line coverage on the core, plus deterministic simulation
37
+ testing that tortures the write-ahead log under a simulated crashing filesystem.
38
+ - **Nothing hidden** — queries are plain in-engine scans, errors surface, and costs are obvious (O(n)
39
+ scans, no secret indexes).
40
+
41
+ ## Quick start
4
42
 
5
- **LibreDB is a small, readable, embeddable database.**
6
-
7
- It is built on one idea: a database can be powerful and still remain understandable. You should be able to open its source, learn how a database actually works, and embed it into your own product in an afternoon.
8
-
9
- - A single small storage core (`core.ts`), with relational, document, and key-value as thin *lenses* on top — not three separate engines (FoundationDB-style architecture).
10
- - Written in TypeScript, shipped on npm.
11
- - Open at the edges, guarded at the durability core. Every line of the core is tested.
12
-
13
- LibreDB is the database in a three-product family that shares one spine:
14
-
15
- - **LibreDB** — the database (this repository).
16
- - **LibreDB Studio** — the open-source IDE for every database (Postgres, MySQL, MongoDB, Redis, and more). LibreDB is one database it supports natively, not a requirement.
17
- - **LibreDB Platform** — the managed, team-oriented form of data.
18
-
19
- ## Usage — the key-value lens
20
-
21
- The first lens is `kv`: a durable, ordered, string-keyed map over the kernel. You `open` a kernel database and put a `kv` lens over it.
22
-
23
- ```ts
24
- import { open, kv } from "libredb";
25
-
26
- // In-memory: the natural fit for tests and ephemeral use.
27
- const db = open();
28
-
29
- // Or file-backed and durable. Every write is appended to a write-ahead
30
- // log and fsync'd before it returns, so a committed write survives a
31
- // crash; reopening the same path reconstructs exactly the committed state:
32
- //
33
- // const db = open({ path: "data.libredb" });
34
-
35
- const store = kv(db);
36
-
37
- store.set("user:1", "Ada");
38
- store.set("user:2", "Grace");
39
-
40
- store.get("user:1"); // "Ada"
41
- store.get("missing"); // undefined
42
-
43
- db.close();
44
- ```
45
-
46
- ### Writes report what they changed
47
-
48
- Every write returns a `WriteResult` (`{ changed }`), so a caller can always see what the write actually did:
49
-
50
- ```ts
51
- store.set("user:1", "Ada Lovelace").changed; // 1 (overwrote the existing value)
52
-
53
- store.delete("user:2").changed; // 1 (it existed)
54
- store.delete("user:2").changed; // 0 (already gone)
55
- ```
56
-
57
- ### Ordered scans
58
-
59
- Keys are ordered by byte order, so reads come back in ascending key order. `range(start, end)` scans the half-open interval `[start, end)` — `start` included, `end` excluded:
60
-
61
- ```ts
62
- store.set("a", "1");
63
- store.set("b", "2");
64
- store.set("c", "3");
65
-
66
- store.range("a", "c").toArray();
67
- // [{ key: "a", value: "1" }, { key: "b", value: "2" }] — "c" is excluded
68
- ```
69
-
70
- `prefix(p)` scans every key beginning with `p` — the canonical ordered-KV query:
71
-
72
- ```ts
73
- store.prefix("user:").toArray();
74
- // [{ key: "user:1", value: "Ada Lovelace" }]
75
- ```
76
-
77
- A read returns a `Result`, which is **lazy and re-iterable**: nothing runs until you iterate it (or call `toArray()`), and each pass re-runs the scan against the current state. Iterate it directly or materialize it:
78
-
79
- ```ts
80
- for (const { key, value } of store.prefix("user:")) {
81
- console.log(key, value);
82
- }
83
- ```
84
-
85
- ### Multi-key atomicity
86
-
87
- Each `kv` operation auto-commits in its own transaction. When you need several writes to apply atomically, drop to the kernel's `transact` directly — it commits all of the body's writes together, or nothing if the body throws:
88
-
89
- ```ts
90
- const encode = new TextEncoder();
91
-
92
- db.transact((tx) => {
93
- tx.set(encode.encode("from"), encode.encode("90"));
94
- tx.set(encode.encode("to"), encode.encode("110"));
95
- }); // both writes commit together, or neither does
96
- ```
97
-
98
- The kernel speaks raw bytes on purpose; the `kv` lens is the string-ergonomic face over it.
99
-
100
- ## Usage — the document lens
101
-
102
- The `doc` lens is the JSON face over the same kernel: a *collection* of JSON documents, each stored under a string `id`. Where `kv` stores strings, `doc` stores objects — numbers stay numbers, and nested objects and arrays survive the round-trip.
103
-
104
- ```ts
105
- import { open, doc } from "libredb";
106
-
107
- const db = open(); // or open({ path: "data.libredb" }) for durability
108
-
109
- const users = doc(db, "users");
110
-
111
- users.put("1", { name: "Ada", age: 36, active: true });
112
- users.put("2", { name: "Grace", age: 45, active: false });
113
-
114
- users.get("1"); // { name: "Ada", age: 36, active: true }
115
- users.get("missing"); // undefined
116
-
117
- db.close();
118
- ```
119
-
120
- Documents are scoped to their collection: `doc(db, "users")` never sees a `doc(db, "orders")` document — and the boundary is exact, so `users` and `users2` stay fully separate.
121
-
122
- ### Writes report what they changed
123
-
124
- Like the kv lens, every write returns a `WriteResult` (`{ changed }`):
125
-
126
- ```ts
127
- users.put("1", { name: "Ada Lovelace", age: 36 }).changed; // 1 (overwrote the existing document)
128
-
129
- users.delete("2").changed; // 1 (it existed)
130
- users.delete("2").changed; // 0 (already gone)
131
- ```
132
-
133
- ### Scanning and finding
134
-
135
- `all()` returns every document in the collection as `{ id, doc }` rows, in ascending `id` (byte) order:
136
-
137
- ```ts
138
- const people = doc(open(), "people");
139
-
140
- people.put("1", { name: "Ada", team: "research", active: true });
141
- people.put("2", { name: "Grace", team: "research", active: false });
142
- people.put("3", { name: "Edsger", team: "ops", active: true });
143
-
144
- people.all().toArray();
145
- // [
146
- // { id: "1", doc: { name: "Ada", team: "research", active: true } },
147
- // { id: "2", doc: { name: "Grace", team: "research", active: false } },
148
- // { id: "3", doc: { name: "Edsger", team: "ops", active: true } },
149
- // ]
43
+ ```sh
44
+ bun add @libredb/libredb
45
+ # or: npm install @libredb/libredb
150
46
  ```
151
47
 
152
- `find(predicate)` returns the documents whose top-level fields all equal the predicate's. Multiple fields are AND'd, and equality is deep and type-sensitive — `1` does not match `"1"`:
48
+ LibreDB is ESM-only, ships zero runtime dependencies, and targets Bun (the development runtime) and
49
+ Node 22+. The same database speaks all three lenses — here they are in one file:
153
50
 
154
51
  ```ts
155
- people.find({ team: "research", active: true }).toArray();
156
- // [{ id: "1", doc: { name: "Ada", team: "research", active: true } }]
157
- ```
158
-
159
- `find` is an O(n) full collection scan with an in-engine predicate — there are no secondary indexes (a deliberate v1 omission, so you can read exactly what a query costs). Like every read it returns a lazy, re-iterable `Result`, so you can iterate it directly or call `toArray()`.
52
+ import { open, kv, doc, table } from "@libredb/libredb";
160
53
 
161
- ## Usage the relational lens
162
-
163
- The `table` lens is the *typed* face over the same kernel: a table is a schema-validated collection of rows. Where `doc` accepts any JSON object, `table` declares its columns and their types up front and enforces them at insert. A row is stored under `<table>:<pk>`, so a table is literally a schema-validated document collection — it reuses the document codec and the same collection-isolation boundary.
164
-
165
- You declare a schema (the columns, their types, and which one is the primary key) when you open the table:
54
+ // In-memory for tests, or open({ path: "data.libredb" }) for a durable, crash-safe file.
55
+ const db = open();
166
56
 
167
- ```ts
168
- import { open, table } from "libredb";
57
+ // 1. Key-value: a durable, ordered, string-keyed map.
58
+ const cache = kv(db);
59
+ cache.set("user:1", "Ada");
60
+ cache.get("user:1"); // "Ada"
169
61
 
170
- const db = open(); // or open({ path: "data.libredb" }) for durability
62
+ // 2. Document: a collection of JSON documents under string ids.
63
+ const logs = doc(db, "logs");
64
+ logs.put("l1", { level: "info", message: "started", at: 1 });
65
+ logs.find({ level: "info" }).toArray(); // [{ id: "l1", doc: { ... } }]
171
66
 
67
+ // 3. Relational: a schema-validated, typed table with where / select / join.
172
68
  const users = table(db, "users", {
173
69
  primaryKey: "id",
174
- columns: { id: "string", name: "string", age: "number", active: "boolean" },
70
+ columns: { id: "string", name: "string", age: "number" },
175
71
  });
176
-
177
- users.insert({ id: "1", name: "Ada", age: 36, active: true });
178
- users.insert({ id: "2", name: "Grace", age: 45, active: false });
179
-
180
- users.get("1"); // { id: "1", name: "Ada", age: 36, active: true }
181
- users.get("missing"); // undefined
72
+ users.insert({ id: "1", name: "Ada", age: 36 });
73
+ users.where({ name: "Ada" }).select("id", "age").toArray(); // [{ id: "1", age: 36 }]
182
74
 
183
75
  db.close();
184
76
  ```
185
77
 
186
- A column type is one of `"string"`, `"number"`, `"boolean"`, or `"object"` (a plain JSON object). The `primaryKey` must name a declared `"string"` column — it becomes the kernel key.
187
-
188
- ### Validation is strict at insert
189
-
190
- Insert rejects a row that is missing a declared column, has a wrong-typed value, or carries a field the schema does not declare — there is no silent coercion or field-dropping:
191
-
192
- ```ts
193
- users.insert({ id: "3", name: "Edsger" });
194
- // throws: missing required column "age"
195
-
196
- users.insert({ id: "3", name: "Edsger", age: "old", active: true });
197
- // throws: column "age" expected number, got string
198
-
199
- users.insert({ id: "3", name: "Edsger", age: 40, active: true, role: "ops" });
200
- // throws: unknown column "role" (not declared in the table schema)
201
- ```
202
-
203
- ### Writes report what they changed
204
-
205
- Like the other lenses, every write returns a `WriteResult` (`{ changed }`):
206
-
207
- ```ts
208
- users.insert({ id: "1", name: "Ada Lovelace", age: 36, active: true }).changed; // 1 (overwrote)
209
-
210
- users.delete("2").changed; // 1 (it existed)
211
- users.delete("2").changed; // 0 (already gone)
212
- ```
213
-
214
- ### Querying — where, select, join
215
-
216
- Reads return a chainable `Query` (which is itself a lazy, re-iterable `Result`). `where(predicate)` keeps the rows whose top-level fields all equal the predicate's — deep and type-sensitive, just like document `find`, so `36` does not match `"36"`. `select(...columns)` projects each row down to the named columns. They compose in any order:
217
-
218
- ```ts
219
- const people = table(open(), "people", {
220
- primaryKey: "id",
221
- columns: { id: "string", name: "string", team: "string", active: "boolean" },
222
- });
223
-
224
- people.insert({ id: "1", name: "Ada", team: "research", active: true });
225
- people.insert({ id: "2", name: "Grace", team: "research", active: false });
226
- people.insert({ id: "3", name: "Edsger", team: "ops", active: true });
227
-
228
- people.where({ team: "research", active: true }).select("name").toArray();
229
- // [{ name: "Ada" }]
230
- ```
231
-
232
- `join(other, leftField, rightField)` is an inner equi-join via nested loop: it pairs each left row with every right row whose `rightField` equals the left row's `leftField`. The result rows carry every column of both sides, qualified as `table.column`, so the two sides never collide — and `select`/`where` name a qualified column the same way as any other:
233
-
234
- ```ts
235
- const orders = table(db, "orders", {
236
- primaryKey: "id",
237
- columns: { id: "string", userId: "string", total: "number" },
238
- });
239
-
240
- orders.insert({ id: "o1", userId: "1", total: 42 });
241
- orders.insert({ id: "o2", userId: "1", total: 7 });
242
-
243
- users.join(orders, "id", "userId").select("users.name", "orders.total").toArray();
244
- // [
245
- // { "users.name": "Ada Lovelace", "orders.total": 42 },
246
- // { "users.name": "Ada Lovelace", "orders.total": 7 },
247
- // ]
248
- ```
249
-
250
- `where` is O(n) in the table size and `join` is O(n*m) — nested-loop, no indexes (the same deliberate v1 omission as the document lens). Unmatched rows on either side are dropped (inner join), and a left key matching several right rows fans out to several result rows.
251
-
252
- ## The catalog what a database holds
253
-
254
- Open a LibreDB file cold and the bytes alone don't say which lens each namespace belongs to. The catalog records that as you write: a `table` registers `{ kind: "relational", schema }` when its handle is built, and a `doc` collection registers `{ kind: "document" }` on its first write. `catalog(db)` reads the whole registry — a `Map` from namespace name to its entry — so a tool can render faithful per-kind views without guessing:
78
+ Each lens has its own guide: [key-value](./docs/guides/key-value.md) ·
79
+ [document](./docs/guides/document.md) · [relational](./docs/guides/relational.md) ·
80
+ [catalog](./docs/guides/catalog.md).
81
+
82
+ ## How it works: one core, three lenses
83
+
84
+ LibreDB has a single ordered byte key-value kernel (`src/core.ts`). Key-value, document, and relational
85
+ are thin typed *lenses* over it — three faces of the same store. A relational table is physically a
86
+ JSON document collection, which is physically ordered key-value entries built from composite keys like
87
+ `users:42`. The kernel reaches the disk through one injectable filesystem seam, which is also what
88
+ makes deterministic crash testing possible.
89
+
90
+ ```mermaid
91
+ flowchart TB
92
+ subgraph consumers [Consumers]
93
+ App[Your app]
94
+ Studio[LibreDB Studio]
95
+ Platform[LibreDB Platform]
96
+ end
97
+
98
+ API["Public API · index.ts<br/>open · kv · doc · table · catalog"]
99
+
100
+ subgraph lenses [Lenses and shared edges - open, fast to contribute]
101
+ KV[kv<br/>strings]
102
+ DOC[document<br/>JSON]
103
+ REL[relational<br/>typed tables]
104
+ CAT[catalog<br/>self-describing registry]
105
+ end
106
+
107
+ CORE["core.ts - THE KERNEL<br/>ordered byte KV · serializable txns · WAL · crash recovery"]
108
+ FS["FileSystem seam<br/>node:fs adapter - or SimFS for crash tests"]
109
+
110
+ App --> API
111
+ Studio --> API
112
+ Platform --> API
113
+ API --> KV
114
+ API --> DOC
115
+ API --> REL
116
+ API --> CAT
117
+ REL --> DOC
118
+ KV --> CORE
119
+ DOC --> CORE
120
+ REL --> CORE
121
+ CAT --> CORE
122
+ CORE --> FS
123
+ ```
124
+
125
+ **The file boundary is the trust boundary.** Below the line (`core.ts`) is guarded: heavy review and
126
+ deterministic crash tests, because a bug there corrupts data. Above the line (lenses, query, catalog)
127
+ is open and fast to contribute to, because the worst a bug can do is present a bad *view* — it reaches
128
+ the store only through one narrow `transact` port. For the full tour, read
129
+ [`ARCHITECTURE.md`](./ARCHITECTURE.md).
130
+
131
+ ## When to use LibreDB
132
+
133
+ **Reach for it when you want to:**
134
+
135
+ - Back tests and local development with a real, durable, multi-model store instead of mocks.
136
+ - Embed a small database directly in a TypeScript / Bun / Node app with zero infrastructure.
137
+ - Learn how a database works by reading — and hacking — a small, honest codebase.
138
+ - Prototype across key-value, document, and relational shapes without standing up three systems.
139
+
140
+ **Do not use it (yet) when you need:**
141
+
142
+ - A hardened production datastore at scale it is **pre-alpha**; today's beachhead is test/dev.
143
+ - Secondary indexes or a query planner — queries are O(n) scans by design in v1 (on the roadmap).
144
+ - Concurrent multi-process access, replication, or a networked client/server — it is embedded and
145
+ in-process.
146
+ - SQL wire compatibility or an existing-driver ecosystem.
147
+
148
+ These limits are deliberate v1 scope, not hidden gaps — LibreDB's strength comes from what it refuses.
149
+ See the [Manifesto](./MANIFESTO.md).
150
+
151
+ ## Reliability
152
+
153
+ A transaction that returns has been written to a length-framed, CRC-32-checksummed write-ahead log and
154
+ `fsync`'d *before* the commit becomes visible — so a committed write survives a crash, and a crash can
155
+ only ever damage the last, un-fsync'd record (which recovery detects and truncates). This is not just
156
+ asserted: the kernel's crash/recovery path is proven by **deterministic simulation testing**, running
157
+ the real engine against a seeded in-memory filesystem that tears, corrupts, and crashes the log on
158
+ command, then checking that recovery is always a valid committed prefix.
255
159
 
256
- ```ts
257
- import { open, table, doc, catalog } from "libredb";
258
-
259
- const db = open();
260
-
261
- table(db, "users", { primaryKey: "id", columns: { id: "string", age: "number" } });
262
- doc(db, "logs").put("l1", { message: "hello" });
263
-
264
- const registry = catalog(db);
265
- registry.get("users");
266
- // { kind: "relational", schema: { primaryKey: "id", columns: { id: "string", age: "number" } } }
267
- registry.get("logs");
268
- // { kind: "document" }
269
- [...registry.keys()].sort();
270
- // ["logs", "users"]
160
+ ```sh
161
+ bun run test # includes a bounded 50-seed DST run
271
162
  ```
272
163
 
273
- The catalog lives under a reserved key prefix that sorts below all user data, so its entries never appear in a `kv`, `doc`, or `table` scan — and no user row leaks into the registry. `kv` namespaces are deliberately not cataloged: `kv` is the raw layer with full keyspace access.
164
+ The full durability and DST walkthrough is in [`docs/RELIABILITY.md`](./docs/RELIABILITY.md).
274
165
 
275
- ## Reliability — deterministic simulation testing
166
+ ## Documentation
276
167
 
277
- Durability is the trust-critical promise: a transaction that returns has been fsync'd, and a crash can only ever damage the last, un-fsync'd record. LibreDB proves this with **deterministic simulation testing (DST)** — the WAL's crash/recovery path is tortured under a seeded, in-memory simulated filesystem. The kernel reaches the disk only through an injectable FS seam (`open({ path, fs })`), so a test can run the real engine on a `SimFS` that crashes on command, tears the un-fsync'd tail at a seeded point, and injects CRC corruption or short reads.
168
+ | Topic | Where |
169
+ |-------|-------|
170
+ | Lens guides (kv, document, relational, catalog) | [`docs/guides/`](./docs/guides/) |
171
+ | Architecture — the guided tour under the hood | [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
172
+ | Design — the locked engineering decisions | [`docs/DESIGN.md`](./docs/DESIGN.md) |
173
+ | Reliability — durability and crash recovery | [`docs/RELIABILITY.md`](./docs/RELIABILITY.md) |
174
+ | Manifesto — what LibreDB is and refuses to be | [`MANIFESTO.md`](./MANIFESTO.md) |
175
+ | LibreDB Studio integration | [`docs/STUDIO.md`](./docs/STUDIO.md) |
278
176
 
279
- Every run is driven by one integer seed and a seeded workload of `set`/`delete`/`transact` operations. After the crash and reopen, recovery must reproduce **a valid committed prefix** of the workload — every transaction that returned successfully, and never a torn or un-committed one. An independent committed-map model (sharing no code with the engine) is the oracle the recovered state is compared against.
177
+ ## Project status & roadmap
280
178
 
281
- The DST suite runs as part of the normal gate:
179
+ LibreDB is **pre-alpha** (`0.0.x`). The architecture is in place and every line of the core is tested,
180
+ but the API may still change and it is not yet meant for production data.
282
181
 
283
- ```sh
284
- bun run test # runs a bounded 50 seeds (fast, CI-friendly)
285
- ```
182
+ - **Done:** the ordered key-value kernel (transactions, WAL, crash recovery); the key-value, document,
183
+ and relational lenses; the self-describing catalog; the deterministic simulation testing harness;
184
+ 100% line/function/statement coverage on the core.
185
+ - **Next:** secondary indexes and a richer query surface; more query operators; additional lenses;
186
+ production-hardening milestones (directory fsync on first create, WAL compaction/checkpointing).
286
187
 
287
- Run a longer soak by raising the seed count (and optionally the base seed):
188
+ ## The LibreDB family
288
189
 
289
- ```sh
290
- LIBREDB_DST_SEEDS=5000 bun test src/sim/dst.test.ts
291
- LIBREDB_DST_BASE=1000000 LIBREDB_DST_SEEDS=5000 bun test src/sim/dst.test.ts
292
- ```
190
+ LibreDB is the database in a three-product family that shares one access-model spine:
293
191
 
294
- When a seed fails, the suite prints the exact replay hint — `Replay with runSeed(<seed>)`. Because everything is seed-driven, one seed reproduces the whole run byte-for-byte:
295
-
296
- ```ts
297
- import { runSeed } from "./src/sim/dst.ts";
192
+ - **LibreDB** the database (this repository).
193
+ - **LibreDB Studio** — the open-source IDE for *every* database (Postgres, MySQL, MongoDB, Redis, and
194
+ more). LibreDB is one database it supports natively, not a requirement.
195
+ - **LibreDB Platform** the managed, team-oriented form of data.
298
196
 
299
- const result = runSeed(42);
300
- result.passed; // true — recovered state is a valid committed prefix
301
- result.recovered; // Map of the recovered committed key-value state
302
- result.expected; // the model oracle's committed state
303
- ```
197
+ ## Contributing
304
198
 
305
- The DST harness lives in `src/sim/` and is excluded from the published package it is test machinery, not shipped code.
199
+ Contributions are welcome. LibreDB is open at the edges and guarded at the durability core see
200
+ [`CONTRIBUTING.md`](./CONTRIBUTING.md) for how to get set up, the `bun run gate` bar every change must
201
+ pass, and where contributions land fastest. Please also read the
202
+ [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md).
306
203
 
307
- ## Read first
204
+ ## Security
308
205
 
309
- - [`MANIFESTO.md`](./MANIFESTO.md) — what LibreDB is and what it refuses to be.
310
- - [`DESIGN.md`](./DESIGN.md) — the engineering decision record behind the manifesto.
206
+ To report a security vulnerability, see [`SECURITY.md`](./SECURITY.md) — please do not open a public
207
+ issue.
311
208
 
312
209
  ## License
313
210
 
314
- Open source and free under the MIT License.
211
+ Open source and free under the [MIT License](./LICENSE).
package/dist/core.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * and discards any record a crash left half-written.
21
21
  */
22
22
  /** The LibreDB package version. Kept in sync with package.json. */
23
- export declare const version = "0.0.2";
23
+ export declare const version = "0.0.4";
24
24
  /**
25
25
  * A key in the kernel: an immutable sequence of bytes.
26
26
  *
package/dist/core.js CHANGED
@@ -19,9 +19,9 @@
19
19
  * transactions, and a write-ahead log for durability. Recovery replays that log
20
20
  * and discards any record a crash left half-written.
21
21
  */
22
- import { closeSync, fsyncSync, openSync, readFileSync, statSync, truncateSync, writeSync, } from "node:fs";
22
+ import { closeSync, fsyncSync, openSync, readFileSync, statSync, truncateSync, writeSync } from "node:fs";
23
23
  /** The LibreDB package version. Kept in sync with package.json. */
24
- export const version = "0.0.2";
24
+ export const version = "0.0.4";
25
25
  /**
26
26
  * Compare two keys by unsigned byte-lexicographic order: the first differing
27
27
  * byte decides, and if one key is a prefix of the other the shorter sorts
package/dist/index.d.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * documents). The kernel's byte-level internals (and each lens's private codec)
7
7
  * stay unexported on purpose — the lens is the usable face. The relational lens
8
8
  * ({@link table}) completes the trio. {@link catalog} reads the registry of
9
- * namespaces a database holds, so a tool can render faithful per-kind views.
9
+ * namespaces a database holds, so a tool can render faithful per-kind views;
10
+ * {@link isReservedKey} (with {@link RESERVED_MARKER} / {@link CATALOG_PREFIX})
11
+ * lets a raw-KV tool hide engine-internal keys instead of hardcoding the layout.
10
12
  */
11
13
  export { version, open } from "./core.ts";
12
14
  export type { Database, OpenOptions } from "./core.ts";
@@ -16,6 +18,6 @@ export { doc } from "./lens/document.ts";
16
18
  export type { DocCollection, Doc, DocEntry, JsonValue } from "./lens/document.ts";
17
19
  export { table } from "./lens/relational.ts";
18
20
  export type { Table, TableSchema, Row, ColumnType, Query } from "./lens/relational.ts";
19
- export { catalog } from "./lens/catalog.ts";
21
+ export { catalog, isReservedKey, CATALOG_PREFIX, RESERVED_MARKER } from "./lens/catalog.ts";
20
22
  export type { CatalogEntry, CatalogRegistry } from "./lens/catalog.ts";
21
23
  export type { Result, WriteResult } from "./lens/types.ts";
package/dist/index.js CHANGED
@@ -6,10 +6,12 @@
6
6
  * documents). The kernel's byte-level internals (and each lens's private codec)
7
7
  * stay unexported on purpose — the lens is the usable face. The relational lens
8
8
  * ({@link table}) completes the trio. {@link catalog} reads the registry of
9
- * namespaces a database holds, so a tool can render faithful per-kind views.
9
+ * namespaces a database holds, so a tool can render faithful per-kind views;
10
+ * {@link isReservedKey} (with {@link RESERVED_MARKER} / {@link CATALOG_PREFIX})
11
+ * lets a raw-KV tool hide engine-internal keys instead of hardcoding the layout.
10
12
  */
11
13
  export { version, open } from "./core.js";
12
14
  export { kv } from "./lens/kv.js";
13
15
  export { doc } from "./lens/document.js";
14
16
  export { table } from "./lens/relational.js";
15
- export { catalog } from "./lens/catalog.js";
17
+ export { catalog, isReservedKey, CATALOG_PREFIX, RESERVED_MARKER } from "./lens/catalog.js";
@@ -16,7 +16,7 @@ export declare const RESERVED_MARKER = "\0";
16
16
  * `libredb:catalog:` tail keeps the namespace self-describing if a raw-KV tool
17
17
  * dumps the file before it understands the catalog.
18
18
  */
19
- export declare const CATALOG_PREFIX = "\0libredb:catalog:";
19
+ export declare const CATALOG_PREFIX: string;
20
20
  /**
21
21
  * Reject a user namespace name that begins with the reserved marker — a loud
22
22
  * error, the same class of correctness rule as the prefix-soundness checks the
@@ -27,6 +27,19 @@ export declare const CATALOG_PREFIX = "\0libredb:catalog:";
27
27
  * layer with full keyspace access (DESIGN.md section 6.3).
28
28
  */
29
29
  export declare function assertUserName(name: string): void;
30
+ /**
31
+ * Whether `key` lies in LibreDB's reserved internal namespace — that is, whether
32
+ * it begins with the {@link RESERVED_MARKER}. This is the public contract a tool
33
+ * that renders RAW key-value data (e.g. the LibreDB Studio provider) uses to HIDE
34
+ * engine-internal keys: the catalog today, and any further reserved sub-namespace
35
+ * added under the marker later. Testing the marker rather than the specific
36
+ * {@link CATALOG_PREFIX} is deliberate — a tool that depends on this stays correct
37
+ * if the reserved namespace grows, so it never has to track the byte layout.
38
+ *
39
+ * {@link assertUserName} guarantees no user namespace name begins with the marker,
40
+ * so this predicate partitions reserved keys from user keys with no overlap.
41
+ */
42
+ export declare function isReservedKey(key: string): boolean;
30
43
  /**
31
44
  * The lens a cataloged namespace belongs to, plus (for a relational table) its
32
45
  * schema — the otherwise-unrecoverable interpretation a cold-opening tool needs
@@ -52,6 +52,21 @@ export function assertUserName(name) {
52
52
  throw new Error(`libredb: namespace name ${JSON.stringify(name)} may not start with the reserved catalog marker (U+0000)`);
53
53
  }
54
54
  }
55
+ /**
56
+ * Whether `key` lies in LibreDB's reserved internal namespace — that is, whether
57
+ * it begins with the {@link RESERVED_MARKER}. This is the public contract a tool
58
+ * that renders RAW key-value data (e.g. the LibreDB Studio provider) uses to HIDE
59
+ * engine-internal keys: the catalog today, and any further reserved sub-namespace
60
+ * added under the marker later. Testing the marker rather than the specific
61
+ * {@link CATALOG_PREFIX} is deliberate — a tool that depends on this stays correct
62
+ * if the reserved namespace grows, so it never has to track the byte layout.
63
+ *
64
+ * {@link assertUserName} guarantees no user namespace name begins with the marker,
65
+ * so this predicate partitions reserved keys from user keys with no overlap.
66
+ */
67
+ export function isReservedKey(key) {
68
+ return key.startsWith(RESERVED_MARKER);
69
+ }
55
70
  const utf8 = new TextEncoder();
56
71
  const fromUtf8 = new TextDecoder();
57
72
  /** The kernel key for one namespace's catalog entry: the reserved prefix plus
@@ -116,7 +116,10 @@ export function doc(store, collection) {
116
116
  for (const entry of tx.getRange(start, end)) {
117
117
  const document = decodeDoc(entry.value);
118
118
  if (keep(document)) {
119
- rows.push({ id: fromUtf8.decode(entry.key).slice(prefix.length), doc: document });
119
+ rows.push({
120
+ id: fromUtf8.decode(entry.key).slice(prefix.length),
121
+ doc: document,
122
+ });
120
123
  }
121
124
  }
122
125
  return rows;
@@ -38,6 +38,10 @@ function matchesType(value, type) {
38
38
  case "boolean":
39
39
  return typeof value === "boolean";
40
40
  case "object":
41
+ // typeof null === "object" at runtime, so this null guard is real
42
+ // defensive validation even though the static Row type excludes null:
43
+ // a JS caller (no TypeScript) can still pass null into a column.
44
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
41
45
  return typeof value === "object" && value !== null && !Array.isArray(value);
42
46
  }
43
47
  }
@@ -191,7 +195,10 @@ export function table(store, name, schema) {
191
195
  // drop the document lens's DocEntry id wrapper (the pk lives in the row), then
192
196
  // let where/select compose over it. Each call rebuilds the Query so laziness
193
197
  // and re-iterability carry through from the document scan.
194
- const allRows = () => query(name, () => rows.all().toArray().map((entry) => entry.doc));
198
+ const allRows = () => query(name, () => rows
199
+ .all()
200
+ .toArray()
201
+ .map((entry) => entry.doc));
195
202
  return {
196
203
  name,
197
204
  insert(row) {
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * A lens (kv, document, relational) is a typed view over the kernel in core.ts.
5
5
  * Lenses differ wildly in HOW you ask for data — a kv key range, a document
6
- * filter, a relational SELECT — and that is on purpose: DESIGN.md section 2 (and
7
- * the founding brainstorm in HANDOFF.md) reject a single unified query
6
+ * filter, a relational SELECT — and that is on purpose: DESIGN.md section 2
7
+ * rejects a single unified query
8
8
  * *language* as a lowest-common-denominator trap. So what lenses share is not
9
9
  * the query syntax but the RESULT envelope: every read hands back a
10
10
  * {@link Result}, every write reports a {@link WriteResult}. Pinning these two
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * A lens (kv, document, relational) is a typed view over the kernel in core.ts.
5
5
  * Lenses differ wildly in HOW you ask for data — a kv key range, a document
6
- * filter, a relational SELECT — and that is on purpose: DESIGN.md section 2 (and
7
- * the founding brainstorm in HANDOFF.md) reject a single unified query
6
+ * filter, a relational SELECT — and that is on purpose: DESIGN.md section 2
7
+ * rejects a single unified query
8
8
  * *language* as a lowest-common-denominator trap. So what lenses share is not
9
9
  * the query syntax but the RESULT envelope: every read hands back a
10
10
  * {@link Result}, every write reports a {@link WriteResult}. Pinning these two
package/package.json CHANGED
@@ -1,7 +1,26 @@
1
1
  {
2
2
  "name": "@libredb/libredb",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A small, readable, embeddable, multi-model database. One ordered key-value core, thin model lenses on top.",
5
+ "keywords": [
6
+ "database",
7
+ "embedded",
8
+ "embeddable",
9
+ "key-value",
10
+ "document",
11
+ "relational",
12
+ "multi-model",
13
+ "typescript",
14
+ "bun"
15
+ ],
16
+ "homepage": "https://github.com/libredb/libredb-database#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/libredb/libredb-database.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/libredb/libredb-database/issues"
23
+ },
5
24
  "type": "module",
6
25
  "license": "MIT",
7
26
  "files": [
@@ -19,21 +38,50 @@
19
38
  "import": "./dist/index.js"
20
39
  }
21
40
  },
41
+ "sideEffects": false,
22
42
  "engines": {
23
- "bun": ">=1.3.0"
43
+ "bun": ">=1.3.0",
44
+ "node": ">=22"
24
45
  },
25
46
  "scripts": {
26
47
  "typecheck": "tsc --noEmit",
27
- "lint": "eslint .",
48
+ "format": "biome format src eslint.config.js",
49
+ "format:fix": "biome format --write src eslint.config.js",
50
+ "lint": "oxlint && eslint .",
28
51
  "test": "bun test --coverage",
29
52
  "build": "tsc --project tsconfig.build.json",
30
- "gate": "bun run typecheck && bun run lint && bun run test && bun run build"
53
+ "size": "size-limit",
54
+ "attw": "rm -rf .attw && bun pm pack --quiet --destination .attw && attw .attw/*.tgz --profile esm-only",
55
+ "publint": "publint",
56
+ "audit": "bun audit --ignore GHSA-h67p-54hq-rp68",
57
+ "secrets": "secretlint '**/*'",
58
+ "commitlint": "commitlint",
59
+ "license:check": "sh scripts/license-check.sh",
60
+ "changeset": "changeset",
61
+ "sync-version": "bun scripts/sync-version.ts",
62
+ "changeset:version": "changeset version && bun run sync-version",
63
+ "knip": "knip-bun",
64
+ "knip:production": "knip-bun --production --strict",
65
+ "prepublishOnly": "bun run build && bun run attw && bun run publint",
66
+ "prepare": "git config core.hooksPath .githooks",
67
+ "gate": "bun run typecheck && bun run format && bun run lint && bun run knip && bun run build && bun run size && bun run test"
31
68
  },
32
69
  "devDependencies": {
33
- "@eslint/js": "^9.0.0",
70
+ "@arethetypeswrong/cli": "^0.18.4",
71
+ "@biomejs/biome": "^2.5.1",
72
+ "@changesets/cli": "2.31.0",
73
+ "@commitlint/cli": "21.1.0",
74
+ "@commitlint/config-conventional": "21.1.0",
75
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.2",
76
+ "@size-limit/preset-small-lib": "^12.1.0",
34
77
  "@types/bun": "latest",
35
- "eslint": "^9.0.0",
36
- "typescript": "^5.4.0",
78
+ "eslint": "^10.0.0",
79
+ "knip": "^6.20.0",
80
+ "oxlint": "^1.71.0",
81
+ "publint": "^0.3.21",
82
+ "secretlint": "13.0.2",
83
+ "size-limit": "^12.1.0",
84
+ "typescript": "^6.0.0",
37
85
  "typescript-eslint": "^8.0.0"
38
86
  }
39
87
  }