@libredb/libredb 0.0.3 → 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 -290
- package/dist/core.d.ts +1 -1
- package/dist/core.js +2 -2
- package/dist/lens/catalog.d.ts +1 -1
- 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,326 +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
|
-
|
|
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.
|
|
251
159
|
|
|
252
|
-
|
|
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:
|
|
255
|
-
|
|
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"]
|
|
271
|
-
```
|
|
272
|
-
|
|
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.
|
|
274
|
-
|
|
275
|
-
A tool that renders the raw `kv` layer (so it sees everything, including the catalog) should hide those engine-internal keys. Rather than hardcode the byte layout, import the contract: `isReservedKey(key)` is true for any key in the reserved namespace, and `RESERVED_MARKER` / `CATALOG_PREFIX` are the underlying constants if you need to build a range.
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
import { open, kv, isReservedKey } from "libredb";
|
|
279
|
-
|
|
280
|
-
const db = open();
|
|
281
|
-
// ... user writes through doc/table, which also write catalog entries ...
|
|
282
|
-
|
|
283
|
-
const visible = kv(db).range("", "").toArray().filter((e) => !isReservedKey(e.key));
|
|
284
|
-
// only user keys; catalog entries (under the reserved marker) are filtered out
|
|
160
|
+
```sh
|
|
161
|
+
bun run test # includes a bounded 50-seed DST run
|
|
285
162
|
```
|
|
286
163
|
|
|
287
|
-
|
|
164
|
+
The full durability and DST walkthrough is in [`docs/RELIABILITY.md`](./docs/RELIABILITY.md).
|
|
288
165
|
|
|
289
|
-
|
|
166
|
+
## Documentation
|
|
290
167
|
|
|
291
|
-
|
|
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) |
|
|
292
176
|
|
|
293
|
-
|
|
177
|
+
## Project status & roadmap
|
|
294
178
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
```
|
|
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.
|
|
298
181
|
|
|
299
|
-
|
|
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).
|
|
300
187
|
|
|
301
|
-
|
|
302
|
-
LIBREDB_DST_SEEDS=5000 bun test src/sim/dst.test.ts
|
|
303
|
-
LIBREDB_DST_BASE=1000000 LIBREDB_DST_SEEDS=5000 bun test src/sim/dst.test.ts
|
|
304
|
-
```
|
|
188
|
+
## The LibreDB family
|
|
305
189
|
|
|
306
|
-
|
|
190
|
+
LibreDB is the database in a three-product family that shares one access-model spine:
|
|
307
191
|
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
310
196
|
|
|
311
|
-
|
|
312
|
-
result.passed; // true — recovered state is a valid committed prefix
|
|
313
|
-
result.recovered; // Map of the recovered committed key-value state
|
|
314
|
-
result.expected; // the model oracle's committed state
|
|
315
|
-
```
|
|
197
|
+
## Contributing
|
|
316
198
|
|
|
317
|
-
|
|
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).
|
|
318
203
|
|
|
319
|
-
##
|
|
204
|
+
## Security
|
|
320
205
|
|
|
321
|
-
|
|
322
|
-
|
|
206
|
+
To report a security vulnerability, see [`SECURITY.md`](./SECURITY.md) — please do not open a public
|
|
207
|
+
issue.
|
|
323
208
|
|
|
324
209
|
## License
|
|
325
210
|
|
|
326
|
-
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/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
|
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
|
}
|