@mark1russell7/sqlite 0.0.0-bootstrap.0 → 0.1.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/LICENSE +21 -0
- package/README.md +106 -1
- package/build/database.d.ts +36 -0
- package/build/database.d.ts.map +1 -0
- package/build/database.js +47 -0
- package/build/database.js.map +1 -0
- package/build/event-log.d.ts +52 -0
- package/build/event-log.d.ts.map +1 -0
- package/build/event-log.js +109 -0
- package/build/event-log.js.map +1 -0
- package/build/index.d.ts +12 -0
- package/build/index.js +5 -0
- package/build/migrate.d.ts +38 -0
- package/build/migrate.d.ts.map +1 -0
- package/build/migrate.js +46 -0
- package/build/migrate.js.map +1 -0
- package/build/transaction.d.ts +30 -0
- package/build/transaction.d.ts.map +1 -0
- package/build/transaction.js +36 -0
- package/build/transaction.js.map +1 -0
- package/build/warning.d.ts +32 -0
- package/build/warning.d.ts.map +1 -0
- package/build/warning.js +49 -0
- package/build/warning.js.map +1 -0
- package/package.json +62 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Russell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
1
|
# @mark1russell7/sqlite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Databases on one computer through `node:sqlite`. The package has the connection with its pragmas, the migrations by `user_version`, the write transaction and a typed append-only event log. It has no dependencies.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @mark1russell7/sqlite
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package needs Node.js 22.16 or later.
|
|
10
|
+
|
|
11
|
+
## The connection
|
|
12
|
+
|
|
13
|
+
`openDatabase` opens a `DatabaseSync` and sets its pragmas. It sets the busy timeout first, because the change to WAL needs the lock of the file.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { openDatabase } from "@mark1russell7/sqlite";
|
|
17
|
+
|
|
18
|
+
const db = openDatabase("data/app.db", { synchronous: "NORMAL", busyTimeoutMs: 5000, makeFolder: true });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Option | Default | What it sets |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `wal` | `true` | The write-ahead log. Then readers do not stop a writer. |
|
|
24
|
+
| `synchronous` | `FULL` | The pragma `synchronous`. With WAL, `NORMAL` can lose the last commits at a power cut. The file stays good. |
|
|
25
|
+
| `busyTimeoutMs` | `0` | The time that a statement waits for the lock of a different connection. With `0`, the statement fails at once. |
|
|
26
|
+
| `makeFolder` | `false` | The function makes the folder of the file. `folderMode` sets the mode of the new folders. |
|
|
27
|
+
|
|
28
|
+
Each default other than `wal` is the default of SQLite.
|
|
29
|
+
|
|
30
|
+
## Migrations
|
|
31
|
+
|
|
32
|
+
`migrate` does the steps that a file does not have. The `user_version` of the file is the count of steps that it has. A step is SQL text or a function.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { migrate, type Migration } from "@mark1russell7/sqlite";
|
|
36
|
+
|
|
37
|
+
const steps: Migration[] = [
|
|
38
|
+
"CREATE TABLE notes (id TEXT PRIMARY KEY, text TEXT NOT NULL) STRICT",
|
|
39
|
+
(db) => db.exec("CREATE INDEX notes_text ON notes (text)"),
|
|
40
|
+
];
|
|
41
|
+
migrate(db, steps); // { from: 0, to: 2 }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Each step is one transaction with its new `user_version`. A step that fails rolls back alone.
|
|
45
|
+
- The function reads the version again in each transaction, so two processes can migrate one file at the same time.
|
|
46
|
+
- A file with more steps than the list is from a newer version of the app, and the function throws.
|
|
47
|
+
- Do not change a step that a file can have. Add a new step.
|
|
48
|
+
|
|
49
|
+
## Transactions
|
|
50
|
+
|
|
51
|
+
`transaction` does the work in one `BEGIN IMMEDIATE` transaction and gives the result of the work. The write lock comes at the start, so a different writer cannot cause a failure in the middle of the work.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { transaction } from "@mark1russell7/sqlite";
|
|
55
|
+
|
|
56
|
+
const added = transaction(db, () => {
|
|
57
|
+
db.prepare("INSERT INTO notes VALUES (?, ?)").run("n1", "first");
|
|
58
|
+
return 1;
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- When the work or the commit throws, the function rolls back and throws the same error.
|
|
63
|
+
- The work is synchronous. A work that gives a promise is an error.
|
|
64
|
+
- A call in a transaction throws, because SQLite refuses a nested `BEGIN`.
|
|
65
|
+
- `onRollbackError` gets the error of a rollback that did not end.
|
|
66
|
+
|
|
67
|
+
## The event log
|
|
68
|
+
|
|
69
|
+
`openEventLog` opens a typed append-only log of events in one file. The caller keeps a state, and `accept` changes it before the write. Thus the state and the file stay equal.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { openEventLog, type LogRecord } from "@mark1russell7/sqlite";
|
|
73
|
+
|
|
74
|
+
interface Added extends LogRecord {
|
|
75
|
+
readonly type: "added";
|
|
76
|
+
readonly name: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const names = new Set<string>();
|
|
80
|
+
const accept = (event: Added): string | undefined => {
|
|
81
|
+
if (names.has(event.name)) return "duplicate";
|
|
82
|
+
names.add(event.name);
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const log = openEventLog("data/events.db", (value) => value as Added);
|
|
87
|
+
for (const event of log.read()) accept(event);
|
|
88
|
+
log.append({ id: "e1", at: new Date().toISOString(), type: "added", name: "a" }, accept);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `append` and `appendAll` give the problems of the refused events. A refused event is not written.
|
|
92
|
+
- A failed write or `accept` rolls back and breaks the log. Each later call throws until `recover`. Then the caller replays the log.
|
|
93
|
+
- `recover` throws when the rollback did not end. Then open the log again.
|
|
94
|
+
- The log owns the `user_version` of its file. `EVENT_LOG_SCHEMA_VERSION` is the version that it writes.
|
|
95
|
+
- The third argument has the options of `openDatabase`.
|
|
96
|
+
|
|
97
|
+
## The experimental warning
|
|
98
|
+
|
|
99
|
+
Node.js 22 writes "SQLite is an experimental feature" to the standard error at the first load of `node:sqlite`. The entry `@mark1russell7/sqlite/warning` removes only that warning. The entry does not import `node:sqlite`.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { quietSqliteWarning } from "@mark1russell7/sqlite/warning";
|
|
103
|
+
|
|
104
|
+
quietSqliteWarning();
|
|
105
|
+
const { openDatabase } = await import("@mark1russell7/sqlite");
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A static import of `node:sqlite` loads before the code of the module. Thus import the database code after the filter, with `import()`. The function gives a function that removes the filter.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
/** The values of the pragma `synchronous`. */
|
|
3
|
+
export type Synchronous = "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
4
|
+
/** The options of `openDatabase`. Each default is the default of SQLite, but the journal is WAL by default. */
|
|
5
|
+
export interface OpenOptions {
|
|
6
|
+
/**
|
|
7
|
+
* This value turns on the write-ahead log (WAL). The default is true. Then readers do not stop a writer. A database
|
|
8
|
+
* with no file keeps its journal in memory.
|
|
9
|
+
*/
|
|
10
|
+
readonly wal?: boolean | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* This value is the pragma `synchronous`. The default is `FULL`: a commit is on the disk when the call ends. With WAL,
|
|
13
|
+
* `NORMAL` can lose the last commits at a power cut, and the file stays good.
|
|
14
|
+
*/
|
|
15
|
+
readonly synchronous?: Synchronous | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* This value is the time in milliseconds that a statement waits for the lock of a different connection. The default
|
|
18
|
+
* is 0: then the statement fails at once with "database is locked".
|
|
19
|
+
*/
|
|
20
|
+
readonly busyTimeoutMs?: number | undefined;
|
|
21
|
+
/** This value makes the folder of the file when the folder is not there. The default is false. */
|
|
22
|
+
readonly makeFolder?: boolean | undefined;
|
|
23
|
+
/** This value is the mode of the folders that `makeFolder` makes, for example `0o700`. */
|
|
24
|
+
readonly folderMode?: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* This function opens a database, and makes the file when it is not there. `:memory:` gives a database with no file.
|
|
28
|
+
*
|
|
29
|
+
* The function sets the busy timeout first, because the change to WAL needs the lock of the file. When a pragma
|
|
30
|
+
* fails, the function closes the connection and throws the error.
|
|
31
|
+
*
|
|
32
|
+
* @param path - The path of the file, or `:memory:`.
|
|
33
|
+
* @param options - The pragmas and the folder.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openDatabase(path: string, options?: OpenOptions): DatabaseSync;
|
|
36
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","names":[],"sources":["../src/database.ts"],"mappings":";;YAUY;;iBAKK;;;;;WAKN;;;;;WAKA,cAAc;;;;;WAKd;;WAEA;;WAEA;;;;;;;;;;;wBAYK,aAAa,cAAc,UAAS,cAAmB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
// src/database.ts
|
|
5
|
+
/**
|
|
6
|
+
* The connection to a database on one computer: a `DatabaseSync` of `node:sqlite` with the pragmas set.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
const SYNCHRONOUS = [
|
|
11
|
+
"OFF",
|
|
12
|
+
"NORMAL",
|
|
13
|
+
"FULL",
|
|
14
|
+
"EXTRA"
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* This function opens a database, and makes the file when it is not there. `:memory:` gives a database with no file.
|
|
18
|
+
*
|
|
19
|
+
* The function sets the busy timeout first, because the change to WAL needs the lock of the file. When a pragma
|
|
20
|
+
* fails, the function closes the connection and throws the error.
|
|
21
|
+
*
|
|
22
|
+
* @param path - The path of the file, or `:memory:`.
|
|
23
|
+
* @param options - The pragmas and the folder.
|
|
24
|
+
*/
|
|
25
|
+
function openDatabase(path, options = {}) {
|
|
26
|
+
const synchronous = options.synchronous ?? "FULL";
|
|
27
|
+
if (!SYNCHRONOUS.includes(synchronous)) throw new RangeError(`The value of synchronous must be one of ${SYNCHRONOUS.join(", ")}, not ${String(synchronous)}.`);
|
|
28
|
+
const busyTimeoutMs = options.busyTimeoutMs ?? 0;
|
|
29
|
+
if (!Number.isInteger(busyTimeoutMs) || busyTimeoutMs < 0) throw new RangeError(`The busy timeout must be a whole number of milliseconds of 0 or more, not ${String(busyTimeoutMs)}.`);
|
|
30
|
+
if (options.makeFolder === true && path !== ":memory:") mkdirSync(dirname(path), options.folderMode === void 0 ? { recursive: true } : {
|
|
31
|
+
recursive: true,
|
|
32
|
+
mode: options.folderMode
|
|
33
|
+
});
|
|
34
|
+
const db = new DatabaseSync(path);
|
|
35
|
+
try {
|
|
36
|
+
db.exec(`PRAGMA busy_timeout = ${String(busyTimeoutMs)}`);
|
|
37
|
+
if (options.wal ?? true) db.exec("PRAGMA journal_mode = WAL");
|
|
38
|
+
db.exec(`PRAGMA synchronous = ${synchronous}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
db.close();
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
return db;
|
|
44
|
+
}
|
|
45
|
+
export { openDatabase };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","names":[],"sources":["../src/database.ts"],"sourcesContent":["/**\n * The connection to a database on one computer: a `DatabaseSync` of `node:sqlite` with the pragmas set.\n *\n * @packageDocumentation\n */\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\n\n/** The values of the pragma `synchronous`. */\nexport type Synchronous = \"OFF\" | \"NORMAL\" | \"FULL\" | \"EXTRA\";\n\nconst SYNCHRONOUS: readonly Synchronous[] = [\"OFF\", \"NORMAL\", \"FULL\", \"EXTRA\"];\n\n/** The options of `openDatabase`. Each default is the default of SQLite, but the journal is WAL by default. */\nexport interface OpenOptions {\n /**\n * This value turns on the write-ahead log (WAL). The default is true. Then readers do not stop a writer. A database\n * with no file keeps its journal in memory.\n */\n readonly wal?: boolean | undefined;\n /**\n * This value is the pragma `synchronous`. The default is `FULL`: a commit is on the disk when the call ends. With WAL,\n * `NORMAL` can lose the last commits at a power cut, and the file stays good.\n */\n readonly synchronous?: Synchronous | undefined;\n /**\n * This value is the time in milliseconds that a statement waits for the lock of a different connection. The default\n * is 0: then the statement fails at once with \"database is locked\".\n */\n readonly busyTimeoutMs?: number | undefined;\n /** This value makes the folder of the file when the folder is not there. The default is false. */\n readonly makeFolder?: boolean | undefined;\n /** This value is the mode of the folders that `makeFolder` makes, for example `0o700`. */\n readonly folderMode?: number | undefined;\n}\n\n/**\n * This function opens a database, and makes the file when it is not there. `:memory:` gives a database with no file.\n *\n * The function sets the busy timeout first, because the change to WAL needs the lock of the file. When a pragma\n * fails, the function closes the connection and throws the error.\n *\n * @param path - The path of the file, or `:memory:`.\n * @param options - The pragmas and the folder.\n */\nexport function openDatabase(path: string, options: OpenOptions = {}): DatabaseSync {\n const synchronous = options.synchronous ?? \"FULL\";\n if (!SYNCHRONOUS.includes(synchronous)) {\n throw new RangeError(`The value of synchronous must be one of ${SYNCHRONOUS.join(\", \")}, not ${String(synchronous)}.`);\n }\n const busyTimeoutMs = options.busyTimeoutMs ?? 0;\n if (!Number.isInteger(busyTimeoutMs) || busyTimeoutMs < 0) {\n throw new RangeError(`The busy timeout must be a whole number of milliseconds of 0 or more, not ${String(busyTimeoutMs)}.`);\n }\n if (options.makeFolder === true && path !== \":memory:\") {\n mkdirSync(dirname(path), options.folderMode === undefined ? { recursive: true } : { recursive: true, mode: options.folderMode });\n }\n const db = new DatabaseSync(path);\n try {\n db.exec(`PRAGMA busy_timeout = ${String(busyTimeoutMs)}`);\n if (options.wal ?? true) db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(`PRAGMA synchronous = ${synchronous}`);\n } catch (error) {\n db.close();\n throw error;\n }\n return db;\n}\n"],"mappings":";;;;;;;;;AAYA,MAAM,cAAsC;CAAC;CAAO;CAAU;CAAQ;AAAO;;;;;;;;;;AAkC7E,SAAgB,aAAa,MAAc,UAAuB,CAAC,GAAiB;CAClF,MAAM,cAAc,QAAQ,eAAe;CAC3C,IAAI,CAAC,YAAY,SAAS,WAAW,GACnC,MAAM,IAAI,WAAW,2CAA2C,YAAY,KAAK,IAAI,EAAE,QAAQ,OAAO,WAAW,EAAE,EAAE;CAEvH,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,IAAI,CAAC,OAAO,UAAU,aAAa,KAAK,gBAAgB,GACtD,MAAM,IAAI,WAAW,6EAA6E,OAAO,aAAa,EAAE,EAAE;CAE5H,IAAI,QAAQ,eAAe,QAAQ,SAAS,YAC1C,UAAU,QAAQ,IAAI,GAAG,QAAQ,eAAe,KAAA,IAAY,EAAE,WAAW,KAAK,IAAI;EAAE,WAAW;EAAM,MAAM,QAAQ;CAAW,CAAC;CAEjI,MAAM,KAAK,IAAI,aAAa,IAAI;CAChC,IAAI;EACF,GAAG,KAAK,yBAAyB,OAAO,aAAa,GAAG;EACxD,IAAI,QAAQ,OAAO,MAAM,GAAG,KAAK,2BAA2B;EAC5D,GAAG,KAAK,wBAAwB,aAAa;CAC/C,SAAS,OAAO;EACd,GAAG,MAAM;EACT,MAAM;CACR;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed append-only log of events in one SQLite file. The caller keeps a state, and the log keeps the file equal to it.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { OpenOptions } from "./database.js";
|
|
7
|
+
/** The fields that the log reads from each event. The log keeps the full event as JSON. */
|
|
8
|
+
export interface LogRecord {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly at: string;
|
|
11
|
+
readonly type: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* This function accepts an event into the state of the caller, or refuses it. It gives a problem when it refuses the
|
|
15
|
+
* event, and then it must not change the state.
|
|
16
|
+
*/
|
|
17
|
+
export type Accept<E, P> = (event: E) => P | undefined;
|
|
18
|
+
/** An open log. */
|
|
19
|
+
export interface EventLog<E extends LogRecord> {
|
|
20
|
+
/** This function gives each event of the log, in the order of the log. */
|
|
21
|
+
read(): Generator<E>;
|
|
22
|
+
/** This function accepts the event first, and then writes it. A refused event is not written. */
|
|
23
|
+
append<P>(event: E, accept: Accept<E, P>): P | undefined;
|
|
24
|
+
/** This function does `append` for each event, in one transaction. It gives the problems of the refused events. */
|
|
25
|
+
appendAll<P>(events: Iterable<E>, accept: Accept<E, P>): P[];
|
|
26
|
+
/** This function gives the count of events in the log. */
|
|
27
|
+
count(): number;
|
|
28
|
+
/**
|
|
29
|
+
* This function makes a broken log usable again. It throws when the rollback of the failed write did not end, and
|
|
30
|
+
* then the log stays broken. After it, the caller replays the log, so its state is equal to the file again.
|
|
31
|
+
*/
|
|
32
|
+
recover(): void;
|
|
33
|
+
/** This function closes the file. */
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
36
|
+
/** The schema version that `openEventLog` writes: the `user_version` of a log file. */
|
|
37
|
+
export declare const EVENT_LOG_SCHEMA_VERSION: number;
|
|
38
|
+
/**
|
|
39
|
+
* This function opens a log, and makes the file when it is not there. `:memory:` gives a log with no file.
|
|
40
|
+
*
|
|
41
|
+
* The state of the caller and the file stay equal: `accept` comes before the write. A write or an `accept` can fail with an
|
|
42
|
+
* error. Then the transaction rolls back, and the log is broken. Each later call throws until `recover`. The state of the
|
|
43
|
+
* caller can keep the events of the failed write, so the caller replays the log after `recover`.
|
|
44
|
+
*
|
|
45
|
+
* The log owns the `user_version` of its file. A file of a newer schema version gives an error.
|
|
46
|
+
*
|
|
47
|
+
* @param path - The path of the file, or `:memory:`.
|
|
48
|
+
* @param parse - The function that changes a stored JSON value into an event. It throws on a value that is not an event.
|
|
49
|
+
* @param options - The pragmas of the connection. The defaults are the defaults of `openDatabase`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function openEventLog<E extends LogRecord>(path: string, parse: (value: unknown) => E, options?: OpenOptions): EventLog<E>;
|
|
52
|
+
//# sourceMappingURL=event-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-log.d.ts","names":[],"sources":["../src/event-log.ts"],"mappings":";;;;;;;iBAUiB;WACN;WACA;WACA;;;;;;YAOC,OAAO,GAAG,MAAM,OAAO,MAAM;;iBAGxB,SAAS,UAAU;;EAElC,QAAQ,UAAU;;EAElB,OAAO,GAAG,OAAO,GAAG,QAAQ,OAAO,GAAG,KAAK;;EAE3C,UAAU,GAAG,QAAQ,SAAS,IAAI,QAAQ,OAAO,GAAG,KAAK;;EAEzD;;;;;EAKA;;EAEA;;;qBAeW;;;;;;;;;;;;;;wBAeG,aAAa,UAAU,WAAW,cAAc,QAAQ,mBAAmB,GAAG,UAAS,cAAmB,SAAS"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { openDatabase } from "./database.js";
|
|
2
|
+
import { migrate } from "./migrate.js";
|
|
3
|
+
import { transaction } from "./transaction.js";
|
|
4
|
+
// src/event-log.ts
|
|
5
|
+
/**
|
|
6
|
+
* A typed append-only log of events in one SQLite file. The caller keeps a state, and the log keeps the file equal to it.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
/** Each entry is one step of the schema of the log. */
|
|
11
|
+
const MIGRATIONS = [`CREATE TABLE events (
|
|
12
|
+
seq INTEGER PRIMARY KEY,
|
|
13
|
+
id TEXT NOT NULL UNIQUE,
|
|
14
|
+
at TEXT NOT NULL,
|
|
15
|
+
type TEXT NOT NULL,
|
|
16
|
+
body TEXT NOT NULL
|
|
17
|
+
) STRICT`];
|
|
18
|
+
/** The schema version that `openEventLog` writes: the `user_version` of a log file. */
|
|
19
|
+
const EVENT_LOG_SCHEMA_VERSION = MIGRATIONS.length;
|
|
20
|
+
/**
|
|
21
|
+
* This function opens a log, and makes the file when it is not there. `:memory:` gives a log with no file.
|
|
22
|
+
*
|
|
23
|
+
* The state of the caller and the file stay equal: `accept` comes before the write. A write or an `accept` can fail with an
|
|
24
|
+
* error. Then the transaction rolls back, and the log is broken. Each later call throws until `recover`. The state of the
|
|
25
|
+
* caller can keep the events of the failed write, so the caller replays the log after `recover`.
|
|
26
|
+
*
|
|
27
|
+
* The log owns the `user_version` of its file. A file of a newer schema version gives an error.
|
|
28
|
+
*
|
|
29
|
+
* @param path - The path of the file, or `:memory:`.
|
|
30
|
+
* @param parse - The function that changes a stored JSON value into an event. It throws on a value that is not an event.
|
|
31
|
+
* @param options - The pragmas of the connection. The defaults are the defaults of `openDatabase`.
|
|
32
|
+
*/
|
|
33
|
+
function openEventLog(path, parse, options = {}) {
|
|
34
|
+
const db = openDatabase(path, options);
|
|
35
|
+
try {
|
|
36
|
+
migrate(db, MIGRATIONS, { name: "log" });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
db.close();
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
const insert = db.prepare("INSERT INTO events (id, at, type, body) VALUES (?, ?, ?, ?)");
|
|
42
|
+
/** The error of a failed write. A broken log refuses each call until `recover`. */
|
|
43
|
+
let broken;
|
|
44
|
+
/** The error of a rollback that did not end. A log with this error cannot recover. */
|
|
45
|
+
let stuck;
|
|
46
|
+
const onRollbackError = (error) => {
|
|
47
|
+
stuck = error;
|
|
48
|
+
};
|
|
49
|
+
function guard() {
|
|
50
|
+
if (broken !== void 0) throw new Error("The log is broken by an earlier error. Open it again and replay it.", { cause: broken });
|
|
51
|
+
}
|
|
52
|
+
function write(event, accept) {
|
|
53
|
+
const problem = accept(event);
|
|
54
|
+
if (problem !== void 0) return problem;
|
|
55
|
+
insert.run(event.id, event.at, event.type, JSON.stringify(event));
|
|
56
|
+
}
|
|
57
|
+
/** This function does the work in one transaction. A failure after the `BEGIN` breaks the log. */
|
|
58
|
+
function inTransaction(work) {
|
|
59
|
+
guard();
|
|
60
|
+
let began = false;
|
|
61
|
+
try {
|
|
62
|
+
return transaction(db, () => {
|
|
63
|
+
began = true;
|
|
64
|
+
return work();
|
|
65
|
+
}, { onRollbackError });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (began) broken = error;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
*read() {
|
|
73
|
+
guard();
|
|
74
|
+
for (const row of db.prepare("SELECT seq, body FROM events ORDER BY seq").iterate()) try {
|
|
75
|
+
yield parse(JSON.parse(row.body));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`The event at the position ${String(row.seq)} of the log is not valid.`, { cause: error });
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
append(event, accept) {
|
|
81
|
+
return inTransaction(() => write(event, accept));
|
|
82
|
+
},
|
|
83
|
+
appendAll(events, accept) {
|
|
84
|
+
return inTransaction(() => {
|
|
85
|
+
const problems = [];
|
|
86
|
+
for (const event of events) {
|
|
87
|
+
const problem = write(event, accept);
|
|
88
|
+
if (problem !== void 0) problems.push(problem);
|
|
89
|
+
}
|
|
90
|
+
return problems;
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
count() {
|
|
94
|
+
guard();
|
|
95
|
+
return db.prepare("SELECT count(*) AS n FROM events").get().n;
|
|
96
|
+
},
|
|
97
|
+
recover() {
|
|
98
|
+
if (broken === void 0) return;
|
|
99
|
+
if (stuck !== void 0 || db.isTransaction) throw new Error("The rollback of the failed write did not end. Open the log again and replay it.", { cause: stuck ?? broken });
|
|
100
|
+
broken = void 0;
|
|
101
|
+
},
|
|
102
|
+
close() {
|
|
103
|
+
db.close();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export { EVENT_LOG_SCHEMA_VERSION, openEventLog };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=event-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-log.js","names":[],"sources":["../src/event-log.ts"],"sourcesContent":["/**\n * A typed append-only log of events in one SQLite file. The caller keeps a state, and the log keeps the file equal to it.\n *\n * @packageDocumentation\n */\nimport { openDatabase, type OpenOptions } from \"./database.ts\";\nimport { migrate, type Migration } from \"./migrate.ts\";\nimport { transaction } from \"./transaction.ts\";\n\n/** The fields that the log reads from each event. The log keeps the full event as JSON. */\nexport interface LogRecord {\n readonly id: string;\n readonly at: string;\n readonly type: string;\n}\n\n/**\n * This function accepts an event into the state of the caller, or refuses it. It gives a problem when it refuses the\n * event, and then it must not change the state.\n */\nexport type Accept<E, P> = (event: E) => P | undefined;\n\n/** An open log. */\nexport interface EventLog<E extends LogRecord> {\n /** This function gives each event of the log, in the order of the log. */\n read(): Generator<E>;\n /** This function accepts the event first, and then writes it. A refused event is not written. */\n append<P>(event: E, accept: Accept<E, P>): P | undefined;\n /** This function does `append` for each event, in one transaction. It gives the problems of the refused events. */\n appendAll<P>(events: Iterable<E>, accept: Accept<E, P>): P[];\n /** This function gives the count of events in the log. */\n count(): number;\n /**\n * This function makes a broken log usable again. It throws when the rollback of the failed write did not end, and\n * then the log stays broken. After it, the caller replays the log, so its state is equal to the file again.\n */\n recover(): void;\n /** This function closes the file. */\n close(): void;\n}\n\n/** Each entry is one step of the schema of the log. */\nconst MIGRATIONS: readonly Migration[] = [\n `CREATE TABLE events (\n seq INTEGER PRIMARY KEY,\n id TEXT NOT NULL UNIQUE,\n at TEXT NOT NULL,\n type TEXT NOT NULL,\n body TEXT NOT NULL\n ) STRICT`,\n];\n\n/** The schema version that `openEventLog` writes: the `user_version` of a log file. */\nexport const EVENT_LOG_SCHEMA_VERSION: number = MIGRATIONS.length;\n\n/**\n * This function opens a log, and makes the file when it is not there. `:memory:` gives a log with no file.\n *\n * The state of the caller and the file stay equal: `accept` comes before the write. A write or an `accept` can fail with an\n * error. Then the transaction rolls back, and the log is broken. Each later call throws until `recover`. The state of the\n * caller can keep the events of the failed write, so the caller replays the log after `recover`.\n *\n * The log owns the `user_version` of its file. A file of a newer schema version gives an error.\n *\n * @param path - The path of the file, or `:memory:`.\n * @param parse - The function that changes a stored JSON value into an event. It throws on a value that is not an event.\n * @param options - The pragmas of the connection. The defaults are the defaults of `openDatabase`.\n */\nexport function openEventLog<E extends LogRecord>(path: string, parse: (value: unknown) => E, options: OpenOptions = {}): EventLog<E> {\n const db = openDatabase(path, options);\n try {\n migrate(db, MIGRATIONS, { name: \"log\" });\n } catch (error) {\n db.close();\n throw error;\n }\n const insert = db.prepare(\"INSERT INTO events (id, at, type, body) VALUES (?, ?, ?, ?)\");\n /** The error of a failed write. A broken log refuses each call until `recover`. */\n let broken: unknown;\n /** The error of a rollback that did not end. A log with this error cannot recover. */\n let stuck: unknown;\n const onRollbackError = (error: unknown): void => {\n stuck = error;\n };\n\n function guard(): void {\n if (broken !== undefined) throw new Error(\"The log is broken by an earlier error. Open it again and replay it.\", { cause: broken });\n }\n\n function write<P>(event: E, accept: Accept<E, P>): P | undefined {\n const problem = accept(event);\n if (problem !== undefined) return problem;\n insert.run(event.id, event.at, event.type, JSON.stringify(event));\n return undefined;\n }\n\n /** This function does the work in one transaction. A failure after the `BEGIN` breaks the log. */\n function inTransaction<T>(work: () => T): T {\n guard();\n let began = false;\n try {\n return transaction(\n db,\n () => {\n began = true;\n return work();\n },\n { onRollbackError },\n );\n } catch (error) {\n if (began) broken = error;\n throw error;\n }\n }\n\n return {\n *read() {\n guard();\n for (const row of db.prepare(\"SELECT seq, body FROM events ORDER BY seq\").iterate() as Iterable<{ seq: number; body: string }>) {\n try {\n yield parse(JSON.parse(row.body));\n } catch (error) {\n throw new Error(`The event at the position ${String(row.seq)} of the log is not valid.`, { cause: error });\n }\n }\n },\n append(event, accept) {\n return inTransaction(() => write(event, accept));\n },\n appendAll(events, accept) {\n return inTransaction(() => {\n const problems = [];\n for (const event of events) {\n const problem = write(event, accept);\n if (problem !== undefined) problems.push(problem);\n }\n return problems;\n });\n },\n count() {\n guard();\n return (db.prepare(\"SELECT count(*) AS n FROM events\").get() as { n: number }).n;\n },\n recover() {\n if (broken === undefined) return;\n if (stuck !== undefined || db.isTransaction) {\n throw new Error(\"The rollback of the failed write did not end. Open the log again and replay it.\", { cause: stuck ?? broken });\n }\n broken = undefined;\n },\n close() {\n db.close();\n },\n };\n}\n"],"mappings":";;;;;;;;;;AA0CA,MAAM,aAAmC,CACvC;;;;;;YAOF;;AAGA,MAAa,2BAAmC,WAAW;;;;;;;;;;;;;;AAe3D,SAAgB,aAAkC,MAAc,OAA8B,UAAuB,CAAC,GAAgB;CACpI,MAAM,KAAK,aAAa,MAAM,OAAO;CACrC,IAAI;EACF,QAAQ,IAAI,YAAY,EAAE,MAAM,MAAM,CAAC;CACzC,SAAS,OAAO;EACd,GAAG,MAAM;EACT,MAAM;CACR;CACA,MAAM,SAAS,GAAG,QAAQ,6DAA6D;;CAEvF,IAAI;;CAEJ,IAAI;CACJ,MAAM,mBAAmB,UAAyB;EAChD,QAAQ;CACV;CAEA,SAAS,QAAc;EACrB,IAAI,WAAW,KAAA,GAAW,MAAM,IAAI,MAAM,uEAAuE,EAAE,OAAO,OAAO,CAAC;CACpI;CAEA,SAAS,MAAS,OAAU,QAAqC;EAC/D,MAAM,UAAU,OAAO,KAAK;EAC5B,IAAI,YAAY,KAAA,GAAW,OAAO;EAClC,OAAO,IAAI,MAAM,IAAI,MAAM,IAAI,MAAM,MAAM,KAAK,UAAU,KAAK,CAAC;CAElE;;CAGA,SAAS,cAAiB,MAAkB;EAC1C,MAAM;EACN,IAAI,QAAQ;EACZ,IAAI;GACF,OAAO,YACL,UACM;IACJ,QAAQ;IACR,OAAO,KAAK;GACd,GACA,EAAE,gBAAgB,CACpB;EACF,SAAS,OAAO;GACd,IAAI,OAAO,SAAS;GACpB,MAAM;EACR;CACF;CAEA,OAAO;EACL,CAAC,OAAO;GACN,MAAM;GACN,KAAK,MAAM,OAAO,GAAG,QAAQ,2CAA2C,CAAC,CAAC,QAAQ,GAChF,IAAI;IACF,MAAM,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC;GAClC,SAAS,OAAO;IACd,MAAM,IAAI,MAAM,6BAA6B,OAAO,IAAI,GAAG,EAAE,4BAA4B,EAAE,OAAO,MAAM,CAAC;GAC3G;EAEJ;EACA,OAAO,OAAO,QAAQ;GACpB,OAAO,oBAAoB,MAAM,OAAO,MAAM,CAAC;EACjD;EACA,UAAU,QAAQ,QAAQ;GACxB,OAAO,oBAAoB;IACzB,MAAM,WAAW,CAAC;IAClB,KAAK,MAAM,SAAS,QAAQ;KAC1B,MAAM,UAAU,MAAM,OAAO,MAAM;KACnC,IAAI,YAAY,KAAA,GAAW,SAAS,KAAK,OAAO;IAClD;IACA,OAAO;GACT,CAAC;EACH;EACA,QAAQ;GACN,MAAM;GACN,OAAQ,GAAG,QAAQ,kCAAkC,CAAC,CAAC,IAAI,CAAC,CAAmB;EACjF;EACA,UAAU;GACR,IAAI,WAAW,KAAA,GAAW;GAC1B,IAAI,UAAU,KAAA,KAAa,GAAG,eAC5B,MAAM,IAAI,MAAM,mFAAmF,EAAE,OAAO,SAAS,OAAO,CAAC;GAE/H,SAAS,KAAA;EACX;EACA,QAAQ;GACN,GAAG,MAAM;EACX;CACF;AACF"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Databases on one computer through `node:sqlite`: the connection with its pragmas, the migrations by `user_version`,
|
|
3
|
+
* the write transaction and a typed append-only event log. The entry `@mark1russell7/sqlite/warning` has the filter of
|
|
4
|
+
* the experimental warning.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { OpenOptions, Synchronous, openDatabase } from "./database.js";
|
|
9
|
+
import { MigrateOptions, Migrated, Migration, migrate, userVersion } from "./migrate.js";
|
|
10
|
+
import { TransactionDatabase, TransactionOptions, transaction } from "./transaction.js";
|
|
11
|
+
import { Accept, EVENT_LOG_SCHEMA_VERSION, EventLog, LogRecord, openEventLog } from "./event-log.js";
|
|
12
|
+
export { type Accept, EVENT_LOG_SCHEMA_VERSION, type EventLog, type LogRecord, type MigrateOptions, type Migrated, type Migration, type OpenOptions, type Synchronous, type TransactionDatabase, type TransactionOptions, migrate, openDatabase, openEventLog, transaction, userVersion };
|
package/build/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { openDatabase } from "./database.js";
|
|
2
|
+
import { migrate, userVersion } from "./migrate.js";
|
|
3
|
+
import { transaction } from "./transaction.js";
|
|
4
|
+
import { EVENT_LOG_SCHEMA_VERSION, openEventLog } from "./event-log.js";
|
|
5
|
+
export { EVENT_LOG_SCHEMA_VERSION, migrate, openDatabase, openEventLog, transaction, userVersion };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The migrations of a schema by the pragma `user_version`: the version of a file is the count of steps that it has.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
/**
|
|
8
|
+
* One step of a schema: SQL statements, or a function that changes the database. A step operates in the transaction of
|
|
9
|
+
* its migration, so it does not start a transaction.
|
|
10
|
+
*/
|
|
11
|
+
export type Migration = string | ((db: DatabaseSync) => void);
|
|
12
|
+
/** The options of `migrate`. */
|
|
13
|
+
export interface MigrateOptions {
|
|
14
|
+
/** This value is the name of the database in the error for a newer file, for example `log`. The default is `database`. */
|
|
15
|
+
readonly name?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** The versions of a file before and after `migrate`. */
|
|
18
|
+
export interface Migrated {
|
|
19
|
+
readonly from: number;
|
|
20
|
+
readonly to: number;
|
|
21
|
+
}
|
|
22
|
+
/** This function gives the pragma `user_version` of a database. */
|
|
23
|
+
export declare function userVersion(db: DatabaseSync): number;
|
|
24
|
+
/**
|
|
25
|
+
* This function does the steps that a database does not have, in order. The `user_version` of the file is the count of
|
|
26
|
+
* steps that it has.
|
|
27
|
+
*
|
|
28
|
+
* Each step is one `BEGIN IMMEDIATE` transaction with its new `user_version`. A step that fails rolls back alone, and
|
|
29
|
+
* the steps before it stay. In the transaction, the function reads the version again, so two processes can migrate
|
|
30
|
+
* one file at the same time. A file with more steps than `migrations` is from a newer version of the app, and the
|
|
31
|
+
* function throws.
|
|
32
|
+
*
|
|
33
|
+
* @param db - The connection.
|
|
34
|
+
* @param migrations - The steps of the schema. Do not change a step that a file can have: add a new step.
|
|
35
|
+
* @param options - The name of the database in the errors.
|
|
36
|
+
*/
|
|
37
|
+
export declare function migrate(db: DatabaseSync, migrations: readonly Migration[], options?: MigrateOptions): Migrated;
|
|
38
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","names":[],"sources":["../src/migrate.ts"],"mappings":";;;;;;;;;;YAYY,uBAAuB,IAAI;;iBAGtB;;WAEN;;;iBAIM;WACN;WACA;;;wBAIK,YAAY,IAAI;;;;;;;;;;;;;;wBAkBhB,QAAQ,IAAI,cAAc,qBAAqB,aAAa,UAAS,iBAAsB"}
|
package/build/migrate.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { transaction } from "./transaction.js";
|
|
2
|
+
// src/migrate.ts
|
|
3
|
+
/** This function gives the pragma `user_version` of a database. */
|
|
4
|
+
function userVersion(db) {
|
|
5
|
+
return db.prepare("PRAGMA user_version").get().user_version;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* This function does the steps that a database does not have, in order. The `user_version` of the file is the count of
|
|
9
|
+
* steps that it has.
|
|
10
|
+
*
|
|
11
|
+
* Each step is one `BEGIN IMMEDIATE` transaction with its new `user_version`. A step that fails rolls back alone, and
|
|
12
|
+
* the steps before it stay. In the transaction, the function reads the version again, so two processes can migrate
|
|
13
|
+
* one file at the same time. A file with more steps than `migrations` is from a newer version of the app, and the
|
|
14
|
+
* function throws.
|
|
15
|
+
*
|
|
16
|
+
* @param db - The connection.
|
|
17
|
+
* @param migrations - The steps of the schema. Do not change a step that a file can have: add a new step.
|
|
18
|
+
* @param options - The name of the database in the errors.
|
|
19
|
+
*/
|
|
20
|
+
function migrate(db, migrations, options = {}) {
|
|
21
|
+
const name = options.name ?? "database";
|
|
22
|
+
const known = migrations.length;
|
|
23
|
+
const refuseNewer = (version) => {
|
|
24
|
+
if (version > known) throw new Error(`The ${name} has the schema version ${String(version)}, and this code knows only ${String(known)}. Use a newer version of the app.`);
|
|
25
|
+
};
|
|
26
|
+
const from = userVersion(db);
|
|
27
|
+
refuseNewer(from);
|
|
28
|
+
let version = from;
|
|
29
|
+
while (version < known) version = transaction(db, () => {
|
|
30
|
+
const current = userVersion(db);
|
|
31
|
+
refuseNewer(current);
|
|
32
|
+
if (current === known) return current;
|
|
33
|
+
const step = migrations[current];
|
|
34
|
+
if (typeof step === "string") db.exec(step);
|
|
35
|
+
else step(db);
|
|
36
|
+
db.exec(`PRAGMA user_version = ${String(current + 1)}`);
|
|
37
|
+
return current + 1;
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
from,
|
|
41
|
+
to: version
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export { migrate, userVersion };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","names":[],"sources":["../src/migrate.ts"],"sourcesContent":["/**\n * The migrations of a schema by the pragma `user_version`: the version of a file is the count of steps that it has.\n *\n * @packageDocumentation\n */\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { transaction } from \"./transaction.ts\";\n\n/**\n * One step of a schema: SQL statements, or a function that changes the database. A step operates in the transaction of\n * its migration, so it does not start a transaction.\n */\nexport type Migration = string | ((db: DatabaseSync) => void);\n\n/** The options of `migrate`. */\nexport interface MigrateOptions {\n /** This value is the name of the database in the error for a newer file, for example `log`. The default is `database`. */\n readonly name?: string | undefined;\n}\n\n/** The versions of a file before and after `migrate`. */\nexport interface Migrated {\n readonly from: number;\n readonly to: number;\n}\n\n/** This function gives the pragma `user_version` of a database. */\nexport function userVersion(db: DatabaseSync): number {\n const row = db.prepare(\"PRAGMA user_version\").get() as { user_version: number };\n return row.user_version;\n}\n\n/**\n * This function does the steps that a database does not have, in order. The `user_version` of the file is the count of\n * steps that it has.\n *\n * Each step is one `BEGIN IMMEDIATE` transaction with its new `user_version`. A step that fails rolls back alone, and\n * the steps before it stay. In the transaction, the function reads the version again, so two processes can migrate\n * one file at the same time. A file with more steps than `migrations` is from a newer version of the app, and the\n * function throws.\n *\n * @param db - The connection.\n * @param migrations - The steps of the schema. Do not change a step that a file can have: add a new step.\n * @param options - The name of the database in the errors.\n */\nexport function migrate(db: DatabaseSync, migrations: readonly Migration[], options: MigrateOptions = {}): Migrated {\n const name = options.name ?? \"database\";\n const known = migrations.length;\n const refuseNewer = (version: number): void => {\n if (version > known) {\n throw new Error(`The ${name} has the schema version ${String(version)}, and this code knows only ${String(known)}. Use a newer version of the app.`);\n }\n };\n const from = userVersion(db);\n refuseNewer(from);\n let version = from;\n while (version < known) {\n version = transaction(db, () => {\n const current = userVersion(db);\n refuseNewer(current);\n if (current === known) return current;\n const step = migrations[current] as Migration;\n if (typeof step === \"string\") db.exec(step);\n else step(db);\n db.exec(`PRAGMA user_version = ${String(current + 1)}`);\n return current + 1;\n });\n }\n return { from, to: version };\n}\n"],"mappings":";;;AA2BA,SAAgB,YAAY,IAA0B;CAEpD,OADY,GAAG,QAAQ,qBAAqB,CAAC,CAAC,IACrC,CAAC,CAAC;AACb;;;;;;;;;;;;;;AAeA,SAAgB,QAAQ,IAAkB,YAAkC,UAA0B,CAAC,GAAa;CAClH,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,QAAQ,WAAW;CACzB,MAAM,eAAe,YAA0B;EAC7C,IAAI,UAAU,OACZ,MAAM,IAAI,MAAM,OAAO,KAAK,0BAA0B,OAAO,OAAO,EAAE,6BAA6B,OAAO,KAAK,EAAE,kCAAkC;CAEvJ;CACA,MAAM,OAAO,YAAY,EAAE;CAC3B,YAAY,IAAI;CAChB,IAAI,UAAU;CACd,OAAO,UAAU,OACf,UAAU,YAAY,UAAU;EAC9B,MAAM,UAAU,YAAY,EAAE;EAC9B,YAAY,OAAO;EACnB,IAAI,YAAY,OAAO,OAAO;EAC9B,MAAM,OAAO,WAAW;EACxB,IAAI,OAAO,SAAS,UAAU,GAAG,KAAK,IAAI;OACrC,KAAK,EAAE;EACZ,GAAG,KAAK,yBAAyB,OAAO,UAAU,CAAC,GAAG;EACtD,OAAO,UAAU;CACnB,CAAC;CAEH,OAAO;EAAE;EAAM,IAAI;CAAQ;AAC7B"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write transaction: `BEGIN IMMEDIATE`, the work, and `COMMIT`, or a `ROLLBACK` after an error.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
/** The part of a connection that a transaction uses. A test can give a fake. */
|
|
8
|
+
export type TransactionDatabase = Pick<DatabaseSync, "exec" | "isTransaction">;
|
|
9
|
+
/** The options of `transaction`. */
|
|
10
|
+
export interface TransactionOptions {
|
|
11
|
+
/**
|
|
12
|
+
* This function gets the error of a rollback that did not end. Then the connection can stay in the transaction, and
|
|
13
|
+
* each later `BEGIN` throws. The work error stays the error that `transaction` throws.
|
|
14
|
+
*/
|
|
15
|
+
readonly onRollbackError?: ((error: unknown) => void) | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* This function does the work in one write transaction, and gives the result of the work.
|
|
19
|
+
*
|
|
20
|
+
* `BEGIN IMMEDIATE` takes the write lock at the start, so a different writer cannot cause a failure in the middle of
|
|
21
|
+
* the work. The busy timeout of the connection sets the wait for that lock. When the work or the commit throws, the
|
|
22
|
+
* function rolls back and throws the same error. The work is synchronous: a work that gives a promise is an error.
|
|
23
|
+
* A call in a transaction throws, because SQLite refuses a `BEGIN` there.
|
|
24
|
+
*
|
|
25
|
+
* @param db - The connection.
|
|
26
|
+
* @param work - The work. Its writes are part of the transaction.
|
|
27
|
+
* @param options - The function that gets a failed rollback.
|
|
28
|
+
*/
|
|
29
|
+
export declare function transaction<T>(db: TransactionDatabase, work: () => T, options?: TransactionOptions): T;
|
|
30
|
+
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","names":[],"sources":["../src/transaction.ts"],"mappings":";;;;;;;YAQY,sBAAsB,KAAK;;iBAGtB;;;;;WAKN,oBAAoB;;;;;;;;;;;;;;wBAoBf,YAAY,GAAG,IAAI,qBAAqB,YAAY,GAAG,UAAS,qBAA0B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/transaction.ts
|
|
2
|
+
/** This function tells if a value is a promise or a different thenable. */
|
|
3
|
+
function isThenable(value) {
|
|
4
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* This function does the work in one write transaction, and gives the result of the work.
|
|
8
|
+
*
|
|
9
|
+
* `BEGIN IMMEDIATE` takes the write lock at the start, so a different writer cannot cause a failure in the middle of
|
|
10
|
+
* the work. The busy timeout of the connection sets the wait for that lock. When the work or the commit throws, the
|
|
11
|
+
* function rolls back and throws the same error. The work is synchronous: a work that gives a promise is an error.
|
|
12
|
+
* A call in a transaction throws, because SQLite refuses a `BEGIN` there.
|
|
13
|
+
*
|
|
14
|
+
* @param db - The connection.
|
|
15
|
+
* @param work - The work. Its writes are part of the transaction.
|
|
16
|
+
* @param options - The function that gets a failed rollback.
|
|
17
|
+
*/
|
|
18
|
+
function transaction(db, work, options = {}) {
|
|
19
|
+
db.exec("BEGIN IMMEDIATE");
|
|
20
|
+
try {
|
|
21
|
+
const result = work();
|
|
22
|
+
if (isThenable(result)) throw new TypeError("The work of a transaction must be synchronous. The transaction rolled back.");
|
|
23
|
+
db.exec("COMMIT");
|
|
24
|
+
return result;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
try {
|
|
27
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
28
|
+
} catch (rollback) {
|
|
29
|
+
options.onRollbackError?.(rollback);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export { transaction };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","names":[],"sources":["../src/transaction.ts"],"sourcesContent":["/**\n * The write transaction: `BEGIN IMMEDIATE`, the work, and `COMMIT`, or a `ROLLBACK` after an error.\n *\n * @packageDocumentation\n */\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/** The part of a connection that a transaction uses. A test can give a fake. */\nexport type TransactionDatabase = Pick<DatabaseSync, \"exec\" | \"isTransaction\">;\n\n/** The options of `transaction`. */\nexport interface TransactionOptions {\n /**\n * This function gets the error of a rollback that did not end. Then the connection can stay in the transaction, and\n * each later `BEGIN` throws. The work error stays the error that `transaction` throws.\n */\n readonly onRollbackError?: ((error: unknown) => void) | undefined;\n}\n\n/** This function tells if a value is a promise or a different thenable. */\nfunction isThenable(value: unknown): boolean {\n return (typeof value === \"object\" || typeof value === \"function\") && value !== null && typeof (value as { then?: unknown }).then === \"function\";\n}\n\n/**\n * This function does the work in one write transaction, and gives the result of the work.\n *\n * `BEGIN IMMEDIATE` takes the write lock at the start, so a different writer cannot cause a failure in the middle of\n * the work. The busy timeout of the connection sets the wait for that lock. When the work or the commit throws, the\n * function rolls back and throws the same error. The work is synchronous: a work that gives a promise is an error.\n * A call in a transaction throws, because SQLite refuses a `BEGIN` there.\n *\n * @param db - The connection.\n * @param work - The work. Its writes are part of the transaction.\n * @param options - The function that gets a failed rollback.\n */\nexport function transaction<T>(db: TransactionDatabase, work: () => T, options: TransactionOptions = {}): T {\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = work();\n if (isThenable(result)) {\n throw new TypeError(\"The work of a transaction must be synchronous. The transaction rolled back.\");\n }\n db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n if (db.isTransaction) db.exec(\"ROLLBACK\");\n } catch (rollback) {\n options.onRollbackError?.(rollback);\n }\n throw error;\n }\n}\n"],"mappings":";;AAoBA,SAAS,WAAW,OAAyB;CAC3C,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,eAAe,UAAU,QAAQ,OAAQ,MAA6B,SAAS;AACvI;;;;;;;;;;;;;AAcA,SAAgB,YAAe,IAAyB,MAAe,UAA8B,CAAC,GAAM;CAC1G,GAAG,KAAK,iBAAiB;CACzB,IAAI;EACF,MAAM,SAAS,KAAK;EACpB,IAAI,WAAW,MAAM,GACnB,MAAM,IAAI,UAAU,6EAA6E;EAEnG,GAAG,KAAK,QAAQ;EAChB,OAAO;CACT,SAAS,OAAO;EACd,IAAI;GACF,IAAI,GAAG,eAAe,GAAG,KAAK,UAAU;EAC1C,SAAS,UAAU;GACjB,QAAQ,kBAAkB,QAAQ;EACpC;EACA,MAAM;CACR;AACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A filter of the experimental warning of `node:sqlite`. Node.js 22 writes the warning to the standard error at the
|
|
3
|
+
* first load of `node:sqlite`. This file does not import `node:sqlite`, so a command loads it first.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { quietSqliteWarning } from "@mark1russell7/sqlite/warning";
|
|
7
|
+
*
|
|
8
|
+
* quietSqliteWarning();
|
|
9
|
+
* const { openDatabase } = await import("@mark1russell7/sqlite");
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
/** The start of the text of the experimental warning of `node:sqlite`. */
|
|
15
|
+
export declare const SQLITE_WARNING = "SQLite is an experimental feature";
|
|
16
|
+
/** The part of `process` that the filter changes. A test can give a fake. */
|
|
17
|
+
export interface WarningTarget {
|
|
18
|
+
emitWarning(warning: string | Error, ...rest: unknown[]): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* This function tells if the arguments of `process.emitWarning` give the experimental warning of `node:sqlite`. The
|
|
22
|
+
* type of the warning is the second argument, the field `type` of the options, or the name of an `Error`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isSqliteWarning(warning: string | Error, ...rest: unknown[]): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* This function removes only the experimental warning of `node:sqlite` from `emitWarning`. Each other warning stays.
|
|
27
|
+
* Use it before the first import of `node:sqlite`. It gives a function that puts the earlier `emitWarning` back.
|
|
28
|
+
*
|
|
29
|
+
* @param target - The object with `emitWarning`. The default is `process`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function quietSqliteWarning(target?: WarningTarget): () => void;
|
|
32
|
+
//# sourceMappingURL=warning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warning.d.ts","names":[],"sources":["../src/warning.ts"],"mappings":";;;;;;;;;;;;;;qBAea;;iBAGI;EACf,YAAY,kBAAkB,UAAU;;;;;;wBAO1B,gBAAgB,kBAAkB,UAAU;;;;;;;wBAgB5C,mBAAmB,SAAQ"}
|
package/build/warning.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/warning.ts
|
|
2
|
+
/**
|
|
3
|
+
* A filter of the experimental warning of `node:sqlite`. Node.js 22 writes the warning to the standard error at the
|
|
4
|
+
* first load of `node:sqlite`. This file does not import `node:sqlite`, so a command loads it first.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { quietSqliteWarning } from "@mark1russell7/sqlite/warning";
|
|
8
|
+
*
|
|
9
|
+
* quietSqliteWarning();
|
|
10
|
+
* const { openDatabase } = await import("@mark1russell7/sqlite");
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
/** The start of the text of the experimental warning of `node:sqlite`. */
|
|
16
|
+
const SQLITE_WARNING = "SQLite is an experimental feature";
|
|
17
|
+
/**
|
|
18
|
+
* This function tells if the arguments of `process.emitWarning` give the experimental warning of `node:sqlite`. The
|
|
19
|
+
* type of the warning is the second argument, the field `type` of the options, or the name of an `Error`.
|
|
20
|
+
*/
|
|
21
|
+
function isSqliteWarning(warning, ...rest) {
|
|
22
|
+
const first = rest[0];
|
|
23
|
+
let kind;
|
|
24
|
+
if (typeof first === "string") kind = first;
|
|
25
|
+
else if (typeof first === "object" && first !== null && "type" in first) kind = first.type;
|
|
26
|
+
else if (warning instanceof Error) kind = warning.name;
|
|
27
|
+
const text = typeof warning === "string" ? warning : warning.message;
|
|
28
|
+
return kind === "ExperimentalWarning" && text.startsWith("SQLite is an experimental feature");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* This function removes only the experimental warning of `node:sqlite` from `emitWarning`. Each other warning stays.
|
|
32
|
+
* Use it before the first import of `node:sqlite`. It gives a function that puts the earlier `emitWarning` back.
|
|
33
|
+
*
|
|
34
|
+
* @param target - The object with `emitWarning`. The default is `process`.
|
|
35
|
+
*/
|
|
36
|
+
function quietSqliteWarning(target = process) {
|
|
37
|
+
const earlier = target.emitWarning;
|
|
38
|
+
const filtered = (warning, ...rest) => {
|
|
39
|
+
if (isSqliteWarning(warning, ...rest)) return;
|
|
40
|
+
earlier.call(target, warning, ...rest);
|
|
41
|
+
};
|
|
42
|
+
target.emitWarning = filtered;
|
|
43
|
+
return () => {
|
|
44
|
+
if (target.emitWarning === filtered) target.emitWarning = earlier;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export { SQLITE_WARNING, isSqliteWarning, quietSqliteWarning };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=warning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warning.js","names":[],"sources":["../src/warning.ts"],"sourcesContent":["/**\n * A filter of the experimental warning of `node:sqlite`. Node.js 22 writes the warning to the standard error at the\n * first load of `node:sqlite`. This file does not import `node:sqlite`, so a command loads it first.\n *\n * ```ts\n * import { quietSqliteWarning } from \"@mark1russell7/sqlite/warning\";\n *\n * quietSqliteWarning();\n * const { openDatabase } = await import(\"@mark1russell7/sqlite\");\n * ```\n *\n * @packageDocumentation\n */\n\n/** The start of the text of the experimental warning of `node:sqlite`. */\nexport const SQLITE_WARNING = \"SQLite is an experimental feature\";\n\n/** The part of `process` that the filter changes. A test can give a fake. */\nexport interface WarningTarget {\n emitWarning(warning: string | Error, ...rest: unknown[]): void;\n}\n\n/**\n * This function tells if the arguments of `process.emitWarning` give the experimental warning of `node:sqlite`. The\n * type of the warning is the second argument, the field `type` of the options, or the name of an `Error`.\n */\nexport function isSqliteWarning(warning: string | Error, ...rest: unknown[]): boolean {\n const first = rest[0];\n let kind: unknown;\n if (typeof first === \"string\") kind = first;\n else if (typeof first === \"object\" && first !== null && \"type\" in first) kind = first.type;\n else if (warning instanceof Error) kind = warning.name;\n const text = typeof warning === \"string\" ? warning : warning.message;\n return kind === \"ExperimentalWarning\" && text.startsWith(SQLITE_WARNING);\n}\n\n/**\n * This function removes only the experimental warning of `node:sqlite` from `emitWarning`. Each other warning stays.\n * Use it before the first import of `node:sqlite`. It gives a function that puts the earlier `emitWarning` back.\n *\n * @param target - The object with `emitWarning`. The default is `process`.\n */\nexport function quietSqliteWarning(target: WarningTarget = process): () => void {\n const earlier = target.emitWarning;\n const filtered = (warning: string | Error, ...rest: unknown[]): void => {\n if (isSqliteWarning(warning, ...rest)) return;\n earlier.call(target, warning, ...rest);\n };\n target.emitWarning = filtered;\n return () => {\n if (target.emitWarning === filtered) target.emitWarning = earlier;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,iBAAiB;;;;;AAW9B,SAAgB,gBAAgB,SAAyB,GAAG,MAA0B;CACpF,MAAM,QAAQ,KAAK;CACnB,IAAI;CACJ,IAAI,OAAO,UAAU,UAAU,OAAO;MACjC,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,OAAO,OAAO,MAAM;MACjF,IAAI,mBAAmB,OAAO,OAAO,QAAQ;CAClD,MAAM,OAAO,OAAO,YAAY,WAAW,UAAU,QAAQ;CAC7D,OAAO,SAAS,yBAAyB,KAAK,WAAA,mCAAyB;AACzE;;;;;;;AAQA,SAAgB,mBAAmB,SAAwB,SAAqB;CAC9E,MAAM,UAAU,OAAO;CACvB,MAAM,YAAY,SAAyB,GAAG,SAA0B;EACtE,IAAI,gBAAgB,SAAS,GAAG,IAAI,GAAG;EACvC,QAAQ,KAAK,QAAQ,SAAS,GAAG,IAAI;CACvC;CACA,OAAO,cAAc;CACrB,aAAa;EACX,IAAI,OAAO,gBAAgB,UAAU,OAAO,cAAc;CAC5D;AACF"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
2
3
|
"name": "@mark1russell7/sqlite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Databases on one computer through node:sqlite: the pragmas, migrations by user_version, BEGIN IMMEDIATE transactions, a typed append-only event log and a filter of the experimental warning.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sqlite",
|
|
8
|
+
"node:sqlite",
|
|
9
|
+
"wal",
|
|
10
|
+
"migrations",
|
|
11
|
+
"user_version",
|
|
12
|
+
"transaction",
|
|
13
|
+
"event-log",
|
|
14
|
+
"append-only",
|
|
15
|
+
"event-sourcing",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/mark1russell7/sqlite/tree/main/packages/sqlite#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/mark1russell7/sqlite/issues"
|
|
21
|
+
},
|
|
5
22
|
"repository": {
|
|
6
23
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/mark1russell7/sqlite.git"
|
|
24
|
+
"url": "git+https://github.com/mark1russell7/sqlite.git",
|
|
25
|
+
"directory": "packages/sqlite"
|
|
8
26
|
},
|
|
9
27
|
"license": "MIT",
|
|
28
|
+
"author": "Mark Russell (https://github.com/mark1russell7)",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"main": "./build/index.js",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./build/index.d.ts",
|
|
35
|
+
"import": "./build/index.js",
|
|
36
|
+
"default": "./build/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./warning": {
|
|
39
|
+
"types": "./build/warning.d.ts",
|
|
40
|
+
"import": "./build/warning.js",
|
|
41
|
+
"default": "./build/warning.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"build"
|
|
47
|
+
],
|
|
10
48
|
"publishConfig": {
|
|
11
49
|
"access": "public"
|
|
12
|
-
}
|
|
13
|
-
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.16"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@mark1russell7/cue": "^2.0.0",
|
|
56
|
+
"@types/node": "^26.6.5",
|
|
57
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
58
|
+
"esbuild": "^0.28.2",
|
|
59
|
+
"tsdown": "^0.23.7",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^5.0.3"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsdown",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest",
|
|
67
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
|
|
68
|
+
},
|
|
69
|
+
"types": "./build/index.d.ts"
|
|
70
|
+
}
|