@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 +175 -278
- package/dist/core.d.ts +1 -1
- package/dist/core.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/lens/catalog.d.ts +14 -1
- package/dist/lens/catalog.js +15 -0
- package/dist/lens/document.js +4 -1
- package/dist/lens/relational.js +8 -1
- package/dist/lens/types.d.ts +2 -2
- package/dist/lens/types.js +2 -2
- package/package.json +55 -7
package/README.md
CHANGED
|
@@ -1,314 +1,211 @@
|
|
|
1
1
|
# LibreDB
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Multi-model without the magic. One core, three lenses, every line tested.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@libredb/libredb)
|
|
6
|
+
[](https://github.com/libredb/libredb-database/actions/workflows/ci.yml)
|
|
7
|
+
[](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
|
|
8
|
+
[](https://sonarcloud.io/summary/new_code?id=libredb_libredb-database)
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](./package.json)
|
|
12
|
+
[](https://bundlephobia.com/package/@libredb/libredb)
|
|
13
|
+
[](#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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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"
|
|
70
|
+
columns: { id: "string", name: "string", age: "number" },
|
|
175
71
|
});
|
|
176
|
-
|
|
177
|
-
users.
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
```
|
|
257
|
-
|
|
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
|
|
164
|
+
The full durability and DST walkthrough is in [`docs/RELIABILITY.md`](./docs/RELIABILITY.md).
|
|
274
165
|
|
|
275
|
-
##
|
|
166
|
+
## Documentation
|
|
276
167
|
|
|
277
|
-
|
|
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
|
-
|
|
177
|
+
## Project status & roadmap
|
|
280
178
|
|
|
281
|
-
The
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
188
|
+
## The LibreDB family
|
|
288
189
|
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
204
|
+
## Security
|
|
308
205
|
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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";
|
package/dist/lens/catalog.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/lens/catalog.js
CHANGED
|
@@ -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
|
package/dist/lens/document.js
CHANGED
|
@@ -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({
|
|
119
|
+
rows.push({
|
|
120
|
+
id: fromUtf8.decode(entry.key).slice(prefix.length),
|
|
121
|
+
doc: document,
|
|
122
|
+
});
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
return rows;
|
package/dist/lens/relational.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/lens/types.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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/dist/lens/types.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"@
|
|
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": "^
|
|
36
|
-
"
|
|
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
|
}
|