@crvouga/sqlite-mem 1.8.0 → 1.9.0
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 +31 -18
- package/compat/divergences.json +5 -5
- package/compat/scenarios.ts +7 -6
- package/dist/api/database.d.ts +7 -15
- package/dist/api/snapshot.d.ts +21 -0
- package/dist/constraints/check.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6490 -5845
- package/dist/index.js.map +4 -4
- package/dist/indexes/index.d.ts +15 -0
- package/dist/indexes/keys.d.ts +20 -0
- package/dist/serialization/codec.d.ts +2 -2
- package/dist/storage/row.d.ts +6 -3
- package/dist/storage/table.d.ts +10 -0
- package/dist/types/value.d.ts +0 -1
- package/dist/unstable.js +630 -133
- package/dist/unstable.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Requires Node.js ≥ 20 or Bun ≥ 1.1. The published package is **ESM only** (`
|
|
|
41
41
|
## Usage
|
|
42
42
|
|
|
43
43
|
```ts
|
|
44
|
-
import { Database } from "@crvouga/sqlite-mem";
|
|
44
|
+
import { Database, Snapshot } from "@crvouga/sqlite-mem";
|
|
45
45
|
|
|
46
46
|
const db = new Database();
|
|
47
47
|
|
|
@@ -57,9 +57,10 @@ db.prepare(`INSERT INTO users (name) VALUES (?)`).run("Alice");
|
|
|
57
57
|
const users = db.query<{ id: number; name: string }>(`SELECT * FROM users`);
|
|
58
58
|
console.log(users);
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
const db2 =
|
|
62
|
-
|
|
60
|
+
const seed = db.snapshot();
|
|
61
|
+
const db2 = seed.open();
|
|
62
|
+
const bytes = seed.encode();
|
|
63
|
+
const db3 = Snapshot.decode(bytes).open();
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
All methods are **synchronous** — do not `await` them. Browser and Node/Bun share the same in-memory JS surface (no filesystem; `ATTACH` opens a new empty in-memory schema, not a file).
|
|
@@ -79,7 +80,7 @@ From the repo root after that install: `bun run example`.
|
|
|
79
80
|
## API
|
|
80
81
|
|
|
81
82
|
```ts
|
|
82
|
-
import { Database, SqliteError } from "@crvouga/sqlite-mem";
|
|
83
|
+
import { Database, Snapshot, SqliteError } from "@crvouga/sqlite-mem";
|
|
83
84
|
|
|
84
85
|
interface DatabaseOptions {
|
|
85
86
|
seed?: number | bigint; // default 1 — ignored when random is "os"
|
|
@@ -93,8 +94,7 @@ interface Database {
|
|
|
93
94
|
query<T = QueryRow>(sql: string, params?: BindValue[]): T[];
|
|
94
95
|
prepare(sql: string): Statement;
|
|
95
96
|
transaction<T>(fn: () => T): T;
|
|
96
|
-
snapshot():
|
|
97
|
-
restore(snapshot: Uint8Array): void;
|
|
97
|
+
snapshot(): Snapshot;
|
|
98
98
|
close(): void;
|
|
99
99
|
[Symbol.dispose]?(): void; // alias for close() when Symbol.dispose exists
|
|
100
100
|
readonly changes: number;
|
|
@@ -121,6 +121,12 @@ interface ResultSet {
|
|
|
121
121
|
lastInsertRowid: number | bigint;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
class Snapshot {
|
|
125
|
+
open(options?: DatabaseOptions): Database;
|
|
126
|
+
encode(): Uint8Array;
|
|
127
|
+
static decode(bytes: Uint8Array): Snapshot;
|
|
128
|
+
}
|
|
129
|
+
|
|
124
130
|
class SqliteError extends Error {
|
|
125
131
|
readonly category: ErrorCategory; // syntax, no_such_table, constraint_unique, misuse, …
|
|
126
132
|
readonly sqliteCode: string; // always set; default "SQLITE_ERROR"
|
|
@@ -128,7 +134,7 @@ class SqliteError extends Error {
|
|
|
128
134
|
}
|
|
129
135
|
```
|
|
130
136
|
|
|
131
|
-
Stick to `Database`, `Statement`, and `SqliteError` for application code. Advanced internals (`parse`, `tokenize`, `evalExpr`, snapshot codec pieces, `SqlValue` utilities, `Prng`, …) are available only from `@crvouga/sqlite-mem/unstable` and are **exempt from semver**.
|
|
137
|
+
Stick to `Database`, `Snapshot`, `Statement`, and `SqliteError` for application code. Advanced internals (`parse`, `tokenize`, `evalExpr`, snapshot codec pieces, `SqlValue` utilities, `Prng`, …) are available only from `@crvouga/sqlite-mem/unstable` and are **exempt from semver**.
|
|
132
138
|
|
|
133
139
|
### Method semantics
|
|
134
140
|
|
|
@@ -138,7 +144,10 @@ Stick to `Database`, `Statement`, and `SqliteError` for application code. Advanc
|
|
|
138
144
|
| `query(sql, params?)` | **Single statement only** (trailing `;` is fine). Returns all rows. Multi-statement scripts throw `misuse`. |
|
|
139
145
|
| `prepare(sql)` | **Single statement only**. Parses immediately; AST is reused. Pass binds as rest args to `run` / `all` / `get` / `result` on each call. |
|
|
140
146
|
| `transaction(fn)` | If idle: `BEGIN` → `fn()` → `COMMIT`, or `ROLLBACK` + rethrow. If already in a transaction: nested savepoint. Nested SQL `BEGIN` still errors. `close()` inside `fn` throws `misuse`. |
|
|
141
|
-
| `snapshot`
|
|
147
|
+
| `snapshot()` | Freeze a reusable {@link Snapshot} template (no encode). Illegal inside a transaction. |
|
|
148
|
+
| `Snapshot.open()` | Copy-on-write fork from a template. Parent stays open. |
|
|
149
|
+
| `Snapshot.encode()` | Lazy SQLM blob for persistence / worker boot (computed once, cached). |
|
|
150
|
+
| `Snapshot.decode(bytes)` | Decode a blob once per `Uint8Array` (WeakMap); later `open()` calls are CoW. |
|
|
142
151
|
| `close()` | Idempotent; rolls back an open SQL transaction; further ops throw `misuse`. Also available as `[Symbol.dispose]` when supported. |
|
|
143
152
|
|
|
144
153
|
SQL `BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT` / `RELEASE` are first-class. Empty / comment-only SQL on `prepare` / `query` throws `misuse` (`empty statement`), matching SQLite prepare failure.
|
|
@@ -174,13 +183,17 @@ Duplicate column names collapse in row objects (last write wins). Use `stmt.resu
|
|
|
174
183
|
|
|
175
184
|
### Snapshots
|
|
176
185
|
|
|
186
|
+
- `db.snapshot()` returns a frozen in-memory {@link Snapshot}. Per-test isolation should `seed.open()` (copy-on-write, ~µs). Encoded bytes are **lazy** via `snapshot.encode()`.
|
|
177
187
|
- Format magic `SQLM` followed by an explicit little-endian format-version `u32` — **not** a portable `.sqlite` file and not loadable by the SQLite CLI.
|
|
178
|
-
- Round-trips ordinary tables, views, indexes, change counters, PRNG state, and clock.
|
|
188
|
+
- Round-trips ordinary tables, views, indexes (SQLM v4 compact index keys; v3 persisted full `IndexStore`; v1/v2 blobs rebuild indexes on hydrate), change counters, PRNG state, and clock.
|
|
179
189
|
- **Not** encoded: triggers, ATTACH’d schemas, virtual tables (FTS / RTREE / …), `userVersion`.
|
|
180
|
-
- Cannot `
|
|
181
|
-
- `
|
|
182
|
-
-
|
|
183
|
-
-
|
|
190
|
+
- Cannot `snapshot()` while a transaction is open.
|
|
191
|
+
- `Snapshot.decode(bytes)` does not mutate the input `Uint8Array`. The same buffer object is decoded once (WeakMap) and later opens are CoW.
|
|
192
|
+
- `open()` shares frozen tables until either side writes; idle `open().snapshot().encode()` is byte-identical to `snapshot().encode()`.
|
|
193
|
+
- `open()` uses a fixed clock from the snapshot unless you pass `{ now: "system" }`, which stays live.
|
|
194
|
+
- Equivalent databases produce byte-identical `encode()` output (schema/rows sorted) **within a single library version**.
|
|
195
|
+
- Per-test isolation (CI-tier, 200 users + 800 items): `Snapshot.open` ~µs; `encode()` / `decode().open()` is the persistence path. See [benchmarks/PERFORMANCE.md](benchmarks/PERFORMANCE.md).
|
|
196
|
+
- **Compatibility policy:** newer library versions can always decode older snapshots; older libraries cannot decode newer format versions (`snapshot_version` / `SQLITE_FORMAT`). Corrupt magic yields a distinct error.
|
|
184
197
|
|
|
185
198
|
## Determinism
|
|
186
199
|
|
|
@@ -189,9 +202,9 @@ The engine is deterministic by default. Invariants:
|
|
|
189
202
|
| Source | Default | Override / notes |
|
|
190
203
|
| --- | --- | --- |
|
|
191
204
|
| `random()` / `randomblob()` | Seeded xorshift64* (`seed: 1`) | `new Database({ seed })` or `{ random: "os" }` for CSPRNG (not rolled back / not restored) |
|
|
192
|
-
| `date('now')` / friends | Fixed `2000-01-01T00:00:00.000Z` | `new Database({ now: Date \| (() => Date) \| "system" })` — `"system"` is wall clock and is **not** frozen by `
|
|
193
|
-
| Table scans | Rowid order | Same order after `snapshot`/`
|
|
194
|
-
| Snapshots | Sorted schema/rows + PRNG state + clock |
|
|
205
|
+
| `date('now')` / friends | Fixed `2000-01-01T00:00:00.000Z` | `new Database({ now: Date \| (() => Date) \| "system" })` — `"system"` is wall clock and is **not** frozen by `open()` |
|
|
206
|
+
| Table scans | Rowid order | Same order after `snapshot`/`open` |
|
|
207
|
+
| Snapshots | Sorted schema/rows + PRNG state + clock | Applied by `open()` into PRNG and `now` |
|
|
195
208
|
| Transactions | PRNG rolls back with `ROLLBACK`/`SAVEPOINT` | Matches data rollback |
|
|
196
209
|
| Numbers | IEEE `-0` canonicalized to `+0` | Bind, affinity, and arithmetic |
|
|
197
210
|
|
|
@@ -238,7 +251,7 @@ This is **not** a drop-in replacement for `sql.js`, `@sqlite.org/sqlite-wasm`, o
|
|
|
238
251
|
2. **No named-object binds and no sticky `bind()`** — pass positional rest args / arrays in declaration order to `query` / `run` / `all` / `get` / `result`.
|
|
239
252
|
3. **`query` / `prepare` are single-statement only** — multi-statement scripts belong in `exec()` (which does not take bind parameters).
|
|
240
253
|
4. **`exec` returns `void` and takes no params** — use `db.prepare(…).run(…)` or `db.query(…)` for binds; use `db.changes` / `stmt.run()` for counters.
|
|
241
|
-
5. **`'now'` is not wall-clock** unless you pass `{ now: "system" }` or `{ now: () => new Date() }`. Default is year 2000. `
|
|
254
|
+
5. **`'now'` is not wall-clock** unless you pass `{ now: "system" }` or `{ now: () => new Date() }`. Default is year 2000. `open()` freezes a snapshot clock except when constructed with `"system"`.
|
|
242
255
|
6. **`random()` is seeded**, not OS entropy, unless you pass `{ random: "os" }`. Snapshots restore the seeded PRNG; OS entropy is not rewound.
|
|
243
256
|
7. **Snapshots are not `.sqlite` files** and do not round-trip FTS / triggers / ATTACH.
|
|
244
257
|
8. **No better-sqlite3 extras** — no `iterate`, `pluck`/`raw`, `safeIntegers` option, `pragma()` helper, `loadExtension`, or SQLite-file `serialize()`.
|
package/compat/divergences.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
{
|
|
12
12
|
"id": "snapshot-sqlm",
|
|
13
13
|
"scope": "snapshot",
|
|
14
|
-
"predicate": "snapshot() bytes are SQLM, not a .sqlite file",
|
|
15
|
-
"specifiedBehavior": "Custom codec; logical Dump after
|
|
14
|
+
"predicate": "snapshot().encode() bytes are SQLM, not a .sqlite file",
|
|
15
|
+
"specifiedBehavior": "Custom codec; logical Dump after Snapshot.open matches pre-snapshot Dump.",
|
|
16
16
|
"pinnedBy": ["SNP-hdr-01", "SNP-rt-01"]
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"id": "snapshot-exclusions",
|
|
83
83
|
"scope": "snapshot",
|
|
84
84
|
"predicate": "triggers, ATTACH, virtual tables, and user_version are not in SQLM",
|
|
85
|
-
"specifiedBehavior": "
|
|
85
|
+
"specifiedBehavior": "decode().open() outcome is pinned per omitted feature.",
|
|
86
86
|
"pinnedBy": ["SNP-omit-01", "SNP-omit-02", "SNP-omit-03", "SNP-omit-04", "TRG-snap-01", "ATT-snap-01", "FTS-snap-01"]
|
|
87
87
|
},
|
|
88
88
|
{
|
|
@@ -116,8 +116,8 @@
|
|
|
116
116
|
{
|
|
117
117
|
"id": "user-version-snapshot",
|
|
118
118
|
"scope": "pragma",
|
|
119
|
-
"predicate": "user_version is not
|
|
120
|
-
"specifiedBehavior": "PRAGMA user_version after
|
|
119
|
+
"predicate": "user_version is not encoded in SQLM",
|
|
120
|
+
"specifiedBehavior": "PRAGMA user_version after decode().open() is the default unless re-set.",
|
|
121
121
|
"pinnedBy": ["PRG-beh-05", "SNP-omit-04"]
|
|
122
122
|
},
|
|
123
123
|
{
|
package/compat/scenarios.ts
CHANGED
|
@@ -371,7 +371,7 @@ const TAIL: CatalogSection[] = [
|
|
|
371
371
|
["now-03", "column DEFAULT CURRENT_TIMESTAMP", D, "README"],
|
|
372
372
|
["now-04", "now system tracks wall clock", D, "README"],
|
|
373
373
|
["now-05", "now fn called per statement", D, "README"],
|
|
374
|
-
["now-06", "
|
|
374
|
+
["now-06", "open freezes clock", D, "README"],
|
|
375
375
|
]),
|
|
376
376
|
section("JSN", "JSON", true, [
|
|
377
377
|
["json-01", "json() minify/validate"],
|
|
@@ -756,15 +756,16 @@ const TAIL: CatalogSection[] = [
|
|
|
756
756
|
["hdr-02", "corrupt magic error"],
|
|
757
757
|
["hdr-03", "truncated blob error"],
|
|
758
758
|
["hdr-04", "future version snapshot_version"],
|
|
759
|
-
["txn-01", "
|
|
760
|
-
["
|
|
761
|
-
["now-
|
|
762
|
-
["
|
|
763
|
-
["rng-01", "restore with random os does not rewind", D],
|
|
759
|
+
["txn-01", "snapshot during txn errors"],
|
|
760
|
+
["now-01", "open with system clock stays live", D],
|
|
761
|
+
["now-02", "open overwrites Date fn with snapshot clock", D],
|
|
762
|
+
["rng-01", "open with random os does not rewind", D],
|
|
764
763
|
["omit-01", "triggers not encoded", D],
|
|
765
764
|
["omit-02", "ATTACH not encoded", D],
|
|
766
765
|
["omit-03", "virtual tables not encoded", D],
|
|
767
766
|
["omit-04", "user_version not encoded", D],
|
|
767
|
+
["open-01", "Snapshot.open CoW fork"],
|
|
768
|
+
["open-02", "encode does not mutate; decode reuses buffer"],
|
|
768
769
|
]),
|
|
769
770
|
section("DET", "Determinism invariants", true, [
|
|
770
771
|
["seed-01", "same seed identical random streams", D],
|
package/dist/api/database.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DatabaseOptions, type RandomMode } from "../runtime/index.js";
|
|
2
2
|
import type { BindValue, QueryRow } from "../types/value.js";
|
|
3
|
+
import { type Snapshot } from "./snapshot.js";
|
|
3
4
|
import { Statement } from "./statement.js";
|
|
4
5
|
/**
|
|
5
6
|
* Pure TypeScript in-memory SQLite database.
|
|
@@ -72,24 +73,14 @@ export declare class Database {
|
|
|
72
73
|
*/
|
|
73
74
|
transaction<T>(fn: () => T): T;
|
|
74
75
|
/**
|
|
75
|
-
*
|
|
76
|
+
* Freeze this database into a reusable {@link Snapshot} template.
|
|
76
77
|
*
|
|
77
|
-
*
|
|
78
|
+
* Does not encode SQLM bytes. Call {@link Snapshot.encode} to persist, or
|
|
79
|
+
* {@link Snapshot.open} for a copy-on-write fork.
|
|
78
80
|
*
|
|
79
|
-
* @throws {SqliteError} If the database is closed.
|
|
80
|
-
*/
|
|
81
|
-
snapshot(): Uint8Array;
|
|
82
|
-
/**
|
|
83
|
-
* Replace this database's contents with a blob from {@link snapshot}.
|
|
84
|
-
*
|
|
85
|
-
* Restores PRNG state and the clock when the snapshot includes them (v2).
|
|
86
|
-
* Newer library versions can restore older snapshots; older libraries cannot
|
|
87
|
-
* restore newer format versions.
|
|
88
|
-
*
|
|
89
|
-
* @param snapshot - Bytes previously returned by {@link snapshot}.
|
|
90
|
-
* @throws {SqliteError} If the database is closed, a transaction is open, or the blob is invalid.
|
|
81
|
+
* @throws {SqliteError} If the database is closed or a transaction is open.
|
|
91
82
|
*/
|
|
92
|
-
|
|
83
|
+
snapshot(): Snapshot;
|
|
93
84
|
/**
|
|
94
85
|
* Close the database. Further SQL throws {@link SqliteError}. Idempotent.
|
|
95
86
|
*
|
|
@@ -117,4 +108,5 @@ export declare class Database {
|
|
|
117
108
|
get totalChanges(): number;
|
|
118
109
|
private prepareSingle;
|
|
119
110
|
}
|
|
111
|
+
export { Snapshot } from "./snapshot.js";
|
|
120
112
|
export type { DatabaseOptions };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DatabaseOptions } from "../runtime/index.js";
|
|
2
|
+
import { type Database } from "./database.js";
|
|
3
|
+
/**
|
|
4
|
+
* Frozen in-memory database template. {@link open} is a copy-on-write fork.
|
|
5
|
+
* Encoded SQLM bytes are produced lazily via {@link encode}.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Snapshot {
|
|
8
|
+
private cachedBytes;
|
|
9
|
+
/** Encoded SQLM blob. Computed once; never mutates a buffer passed to {@link decode}. */
|
|
10
|
+
encode(): Uint8Array;
|
|
11
|
+
/**
|
|
12
|
+
* Copy-on-write database from this frozen template. Does not re-encode or re-decode.
|
|
13
|
+
*/
|
|
14
|
+
open(options?: DatabaseOptions): Database;
|
|
15
|
+
/**
|
|
16
|
+
* Decode `bytes` once and freeze the result. The same `Uint8Array` object
|
|
17
|
+
* returns the same {@link Snapshot} (WeakMap), so later {@link open} calls
|
|
18
|
+
* are copy-on-write forks after the first hydrate.
|
|
19
|
+
*/
|
|
20
|
+
static decode(bytes: Uint8Array): Snapshot;
|
|
21
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Expr } from "../ast/nodes.js";
|
|
2
2
|
import type { IndexStore } from "../indexes/index.js";
|
|
3
3
|
import type { Row, Rowid } from "../storage/row.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { Table } from "../storage/table.js";
|
|
5
5
|
import type { SqlValue } from "../types/value.js";
|
|
6
6
|
export type CheckEvaluator = (expr: Expr, row: Row) => SqlValue;
|
|
7
|
-
export declare function checkNotNull(
|
|
8
|
-
export declare function checkPrimaryKey(
|
|
7
|
+
export declare function checkNotNull(table: Table, row: Row): void;
|
|
8
|
+
export declare function checkPrimaryKey(table: Table, row: Row): void;
|
|
9
9
|
export declare function checkUnique(index: IndexStore, values: readonly SqlValue[], rowid?: Rowid): void;
|
|
10
10
|
export declare function checkExpressions(expressions: readonly Expr[], row: Row, evaluate: CheckEvaluator, constraintName?: string): void;
|
|
11
11
|
export declare function checkTableConstraints(table: Table, row: Row, evaluate: CheckEvaluator): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* @module
|
|
23
23
|
*/
|
|
24
|
-
export { Database, type DatabaseOptions } from "./api/database.js";
|
|
24
|
+
export { Database, Snapshot, type DatabaseOptions } from "./api/database.js";
|
|
25
25
|
export { type RunResult, Statement } from "./api/statement.js";
|
|
26
26
|
export { type ErrorCategory, SqliteError } from "./errors/index.js";
|
|
27
27
|
export type { ResultSet } from "./executor/result.js";
|