@mailwoman/sqlite 9.2.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.
Files changed (62) hide show
  1. package/lib/batched.ts +69 -0
  2. package/lib/client.ts +163 -0
  3. package/lib/database-schema.ts +30 -0
  4. package/lib/dialect-config.ts +30 -0
  5. package/lib/dialect.ts +67 -0
  6. package/lib/driver.ts +175 -0
  7. package/lib/index.ts +25 -0
  8. package/lib/introspection.ts +57 -0
  9. package/lib/schema-columns.ts +52 -0
  10. package/lib/sealed-build.ts +117 -0
  11. package/lib/sealed-db.ts +170 -0
  12. package/lib/sealed.ts +38 -0
  13. package/out/batched.d.ts +40 -0
  14. package/out/batched.d.ts.map +1 -0
  15. package/out/batched.js +41 -0
  16. package/out/batched.js.map +1 -0
  17. package/out/client.d.ts +86 -0
  18. package/out/client.d.ts.map +1 -0
  19. package/out/client.js +85 -0
  20. package/out/client.js.map +1 -0
  21. package/out/database-schema.d.ts +30 -0
  22. package/out/database-schema.d.ts.map +1 -0
  23. package/out/database-schema.js +22 -0
  24. package/out/database-schema.js.map +1 -0
  25. package/out/dialect-config.d.ts +27 -0
  26. package/out/dialect-config.d.ts.map +1 -0
  27. package/out/dialect-config.js +7 -0
  28. package/out/dialect-config.js.map +1 -0
  29. package/out/dialect.d.ts +39 -0
  30. package/out/dialect.d.ts.map +1 -0
  31. package/out/dialect.js +49 -0
  32. package/out/dialect.js.map +1 -0
  33. package/out/driver.d.ts +22 -0
  34. package/out/driver.d.ts.map +1 -0
  35. package/out/driver.js +114 -0
  36. package/out/driver.js.map +1 -0
  37. package/out/index.d.ts +25 -0
  38. package/out/index.d.ts.map +1 -0
  39. package/out/index.js +25 -0
  40. package/out/index.js.map +1 -0
  41. package/out/introspection.d.ts +34 -0
  42. package/out/introspection.d.ts.map +1 -0
  43. package/out/introspection.js +46 -0
  44. package/out/introspection.js.map +1 -0
  45. package/out/schema-columns.d.ts +31 -0
  46. package/out/schema-columns.d.ts.map +1 -0
  47. package/out/schema-columns.js +38 -0
  48. package/out/schema-columns.js.map +1 -0
  49. package/out/sealed-build.d.ts +51 -0
  50. package/out/sealed-build.d.ts.map +1 -0
  51. package/out/sealed-build.js +71 -0
  52. package/out/sealed-build.js.map +1 -0
  53. package/out/sealed-db.d.ts +74 -0
  54. package/out/sealed-db.d.ts.map +1 -0
  55. package/out/sealed-db.js +140 -0
  56. package/out/sealed-db.js.map +1 -0
  57. package/out/sealed.d.ts +27 -0
  58. package/out/sealed.d.ts.map +1 -0
  59. package/out/sealed.js +31 -0
  60. package/out/sealed.js.map +1 -0
  61. package/out/tsconfig.test.tsbuildinfo +1 -0
  62. package/package.json +172 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The sealed polygon-artifact build sequence: one temp file beside the destination, two handles, every
7
+ * cleanup path, then `VACUUM` → seal → atomic swap.
8
+ *
9
+ * THE INGEST AND THE FINISH PHASES USE SEPARATE HANDLES, ALWAYS — including the in-process path, which does
10
+ * not need them separated. The batched path DOES: its children open the same file, so the parent's handle
11
+ * has to be closed across them, and a single shared handle silently becomes a closed one by the time the
12
+ * finish phase runs. Doing it one way in both paths is what puts the fixture suites on the same sequence a
13
+ * national build takes.
14
+ */
15
+
16
+ import { removePathIfPresent } from "@mailwoman/core/fs/writers"
17
+
18
+ import { DatabaseClient } from "#client"
19
+ import { sealDatabase, swapDatabaseIntoPlace } from "#sealed-db"
20
+
21
+ export interface BuildSealedArtifactOptions<DB, Streamed, Result> {
22
+ /**
23
+ * Where the sealed artifact lands. The build writes beside it and swaps.
24
+ */
25
+ out: string
26
+ /**
27
+ * Create every table the build writes — the layer's own, the contract's, and any build-only scratch table (which the
28
+ * finish phase drops before the artifact is sealed).
29
+ */
30
+ createTables: (database: DatabaseClient<DB>) => Promise<void>
31
+ /**
32
+ * The in-process ingest, run under the FIRST handle. Return `undefined` to defer to {@link batched}; any writes made
33
+ * before deferring (attribute preloads) are kept.
34
+ */
35
+ ingest: (database: DatabaseClient<DB>) => Promise<Streamed | undefined>
36
+ /**
37
+ * The bounded child-process ingest, run while the parent holds NO handle. Each child opens the temp file and appends;
38
+ * chunks run one at a time, so there is exactly one writer at every instant.
39
+ */
40
+ batched?: (tmpPath: string) => Promise<Streamed>
41
+ /**
42
+ * Post-ingest work under the SECOND handle: assertions over what was streamed, index/coverage/manifest writes,
43
+ * dropping any scratch table. `VACUUM`, the seal and the swap follow; the artifact's on-disk size is measurable only
44
+ * after this returns and the swap lands.
45
+ */
46
+ finish: (database: DatabaseClient<DB>, streamed: Streamed) => Promise<Result>
47
+ }
48
+
49
+ /**
50
+ * Run one sealed-artifact build.
51
+ *
52
+ * Every failure path removes the temp file: a failed build otherwise leaves a partial multi-gigabyte file whose name
53
+ * carries this process's pid, so nothing would ever pick it up again — the difference between a retry loop that fails
54
+ * and one that fills a disk.
55
+ */
56
+ export async function buildSealedArtifact<DB, Streamed, Result>(
57
+ options: BuildSealedArtifactOptions<DB, Streamed, Result>
58
+ ): Promise<Result> {
59
+ const tmpPath = `${options.out}.tmp-${process.pid}`
60
+
61
+ let streamed: Streamed | undefined
62
+
63
+ {
64
+ await using kdb = new DatabaseClient<DB>(tmpPath)
65
+
66
+ try {
67
+ kdb.exec("PRAGMA journal_mode = OFF")
68
+ kdb.exec("PRAGMA synchronous = OFF")
69
+
70
+ await options.createTables(kdb)
71
+
72
+ streamed = await options.ingest(kdb)
73
+ } catch (error) {
74
+ await kdb.destroy().catch(() => undefined)
75
+ await removePathIfPresent(tmpPath)
76
+
77
+ throw error
78
+ }
79
+ }
80
+
81
+ if (streamed === undefined) {
82
+ if (!options.batched) {
83
+ throw new Error("buildSealedArtifact: the in-process ingest deferred and no batched ingest was supplied")
84
+ }
85
+
86
+ try {
87
+ streamed = await options.batched(tmpPath)
88
+ } catch (error) {
89
+ await removePathIfPresent(tmpPath)
90
+
91
+ throw error
92
+ }
93
+ }
94
+
95
+ const kdb = new DatabaseClient<DB>(tmpPath)
96
+
97
+ try {
98
+ kdb.exec("PRAGMA journal_mode = OFF")
99
+ kdb.exec("PRAGMA synchronous = OFF")
100
+
101
+ const result = await options.finish(kdb, streamed)
102
+
103
+ kdb.exec("VACUUM")
104
+
105
+ await kdb.destroy()
106
+
107
+ await sealDatabase(tmpPath)
108
+ await swapDatabaseIntoPlace(tmpPath, options.out)
109
+
110
+ return result
111
+ } catch (error) {
112
+ await kdb.destroy().catch(() => undefined)
113
+ await removePathIfPresent(tmpPath)
114
+
115
+ throw error
116
+ }
117
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The sealed-artifact invariant: every SQLite DB a build produces is a READ-ONLY asset. `sealDatabase`
7
+ * is the last step of every builder — checkpoint, freeze the journal, chmod 0444. `openBuiltClient`
8
+ * (`@mailwoman/sqlite/sealed`) is how anything opens a data artifact; a write-mode open of a sealed
9
+ * file throws a NAMED error pointing at the rebuild command instead of a cryptic SQLITE_READONLY. Unsealing is deliberate and
10
+ * manual (`chmod u+w`), never programmatic — rebuild, don't mutate.
11
+ *
12
+ * `swapDatabaseIntoPlace` is the other half of that invariant — the atomic publish step AGENTS.md
13
+ * specifies in prose ("build it successfully, then move the previous version to a temp directory,
14
+ * and then move the new version into place"). It lives here because this module already owns the
15
+ * built-artifact lifecycle, and because a rule the project states in prose and implements more than
16
+ * once in code should be a function.
17
+ */
18
+
19
+ import type { DatabaseSync } from "node:sqlite"
20
+
21
+ import { pathExists, statPath } from "@mailwoman/core/fs/readers"
22
+ import { changeMode, movePath, removePath, removePathIfPresent } from "@mailwoman/core/fs/writers"
23
+ import { basename } from "path-ts"
24
+
25
+ /**
26
+ * The one capability {@link assertDatabaseIntegrity} needs. Narrowing to it rather than naming a handle type lets a
27
+ * `DatabaseClient` satisfy the parameter with no import and no cast.
28
+ */
29
+ type IntegrityProbe = Pick<DatabaseSync, "prepare">
30
+
31
+ /**
32
+ * `node:sqlite` via {@link process.getBuiltinModule} — invisible to bundlers. A static import here would ride the
33
+ * `@mailwoman/core/utils` barrel into every consumer, and the docs' plugin loader (which transpiles `docusaurus.config`
34
+ * imports) can't resolve `node:sqlite` (CI: "Cannot find module 'sqlite'"). The builtin accessor keeps `node:sqlite`
35
+ * out of the static import graph with zero resolve surface.
36
+ */
37
+ function sqlite(): typeof import("node:sqlite") {
38
+ return process.getBuiltinModule("node:sqlite")
39
+ }
40
+
41
+ /**
42
+ * A write-mode open was attempted on a sealed (0444) data artifact.
43
+ */
44
+ export class SealedArtifactError extends Error {
45
+ constructor(path: string) {
46
+ super(
47
+ `${basename(path)} is a sealed read-only artifact — rebuild it via \`mailwoman gazetteer build …\`, ` +
48
+ `don't mutate it. (Deliberate unseal: chmod u+w — but prefer a rebuild.)`
49
+ )
50
+
51
+ this.name = "SealedArtifactError"
52
+ }
53
+ }
54
+
55
+ /**
56
+ * True when the artifact exists and carries no write bits (the sealed state {@link sealDatabase} leaves).
57
+ */
58
+ export async function isSealed(path: string): Promise<boolean> {
59
+ return (await pathExists(path)) && ((await statPath(path)).mode & 0o222) === 0
60
+ }
61
+
62
+ /**
63
+ * Finalize a built DB: WAL-checkpoint → `journal_mode = DELETE` → remove `-wal`/`-shm` sidecars → `chmod 0o444`.
64
+ * Idempotent. Throws if the checkpoint cannot complete (another writer holds the DB).
65
+ */
66
+ export async function sealDatabase(path: string): Promise<void> {
67
+ // A previously sealed artifact needs the write bit back for the journal-mode switch.
68
+ if (await isSealed(path)) {
69
+ await changeMode(path, 0o644)
70
+ }
71
+
72
+ let mode: { journal_mode: string }
73
+
74
+ {
75
+ using db = new (sqlite().DatabaseSync)(path)
76
+ const checkpoint = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get() as { busy: number }
77
+
78
+ if (checkpoint.busy !== 0) {
79
+ throw new Error(`sealDatabase: WAL checkpoint busy on ${path} — close all writers first`)
80
+ }
81
+
82
+ mode = db.prepare("PRAGMA journal_mode = DELETE").get() as { journal_mode: string }
83
+ }
84
+
85
+ if (mode.journal_mode !== "delete") {
86
+ throw new Error(`sealDatabase: journal_mode switch failed on ${path} (still ${mode.journal_mode})`)
87
+ }
88
+
89
+ for (const sidecar of [`${path}-wal`, `${path}-shm`]) {
90
+ if (await pathExists(sidecar)) {
91
+ await removePath(sidecar)
92
+ }
93
+ }
94
+
95
+ await changeMode(path, 0o444)
96
+ }
97
+
98
+ /**
99
+ * Verify a freshly-built database is not corrupt, immediately before it is sealed and published.
100
+ *
101
+ * Belongs beside {@link sealDatabase} for the same reason `swapDatabaseIntoPlace` does: the check is part of the
102
+ * built-artifact lifecycle, and every builder was running it from its own copy. `integrity_check` answers with the
103
+ * single row `{ integrity_check: "ok" }` on a healthy file and one row per problem otherwise, so only the first matters
104
+ * — a builder that reads the column and compares it to `"ok"` has done the whole check.
105
+ *
106
+ * @throws When the database reports anything other than `ok`, naming the artifact and what SQLite said.
107
+ */
108
+ export function assertDatabaseIntegrity(db: IntegrityProbe, artifact: string): void {
109
+ const row = db.prepare("PRAGMA integrity_check").get() as { integrity_check: string } | undefined
110
+ const verdict = row?.integrity_check
111
+
112
+ if (verdict !== "ok") {
113
+ throw new Error(`integrity_check failed on ${basename(artifact)}: ${verdict ?? "no result"}`)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Refuse a write-mode open of a sealed artifact, naming the rebuild command instead of letting SQLite answer
119
+ * `SQLITE_READONLY`.
120
+ *
121
+ * The open itself lives in `@mailwoman/sqlite/sealed` — `openBuiltClient`, which every caller uses. This half stays
122
+ * here because it is a filesystem predicate, and because this module reaches `node:sqlite` through
123
+ * {@link process.getBuiltinModule} to keep the `@mailwoman/core/utils` barrel free of it.
124
+ *
125
+ * @throws {SealedArtifactError} When `path` is sealed.
126
+ */
127
+ export async function assertUnsealedForWrite(path: string): Promise<void> {
128
+ if (await isSealed(path)) throw new SealedArtifactError(path)
129
+ }
130
+
131
+ /**
132
+ * Atomically move a freshly-built database into its published location.
133
+ *
134
+ * The build writes to a temp path, so a mid-build crash never leaves a half-written DB at `finalPath`. This moves any
135
+ * prior version aside, slots the new one in, then drops the old — the previous file stays intact until the replacement
136
+ * is committed, and the `-wal`/`-shm` siblings of BOTH paths are cleared so a stale journal can never be paired with a
137
+ * new main file.
138
+ *
139
+ * Sealing (`sealDatabase`) happens on the temp file BEFORE the swap: a sealed artifact is what gets published, and 0444
140
+ * does not prevent a rename.
141
+ */
142
+ export async function swapDatabaseIntoPlace(tmpPath: string, finalPath: string): Promise<void> {
143
+ const aside = `${finalPath}.old-${process.pid}`
144
+
145
+ if (await pathExists(finalPath)) {
146
+ await movePath(finalPath, aside)
147
+ }
148
+
149
+ for (const sfx of ["-wal", "-shm"]) {
150
+ await removePathIfPresent(finalPath + sfx)
151
+ }
152
+
153
+ try {
154
+ await movePath(tmpPath, finalPath)
155
+ } catch (error) {
156
+ // The prior version is already aside at this point — a failed forward rename must not leave
157
+ // the slot empty while a restorable artifact sits one rename away.
158
+ if ((await pathExists(aside)) && !(await pathExists(finalPath))) {
159
+ await movePath(aside, finalPath)
160
+ }
161
+
162
+ throw error
163
+ }
164
+
165
+ for (const sfx of ["-wal", "-shm"]) {
166
+ await removePathIfPresent(tmpPath + sfx)
167
+ }
168
+
169
+ await removePathIfPresent(aside)
170
+ }
package/lib/sealed.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Opening a built data artifact.
7
+ *
8
+ * This is the other half of the sealed-artifact invariant in `../utils/sealed-db.ts`: that module owns the
9
+ * filesystem side (seal, unseal-refusal, atomic swap) and stays free of a static `node:sqlite` import, because it
10
+ * rides the `@mailwoman/core/utils` barrel into consumers that cannot resolve one. The open lives here, where
11
+ * `DatabaseClient` is already imported and the cost is paid. The open is asynchronous: the write branch awaits the
12
+ * seal check against the filesystem before the handle is constructed.
13
+ */
14
+
15
+ import { DatabaseClient } from "#client"
16
+ import type { Database } from "#database-schema"
17
+ import { assertUnsealedForWrite } from "#sealed-db"
18
+
19
+ /**
20
+ * Open a built data artifact. Read-only by default; `write: true` is for builders working on UNsealed staging, and
21
+ * throws `SealedArtifactError` against a sealed file.
22
+ *
23
+ * Together with `new DatabaseClient(path)` this is the whole set of ways a connection comes into being. `DatabaseSync`
24
+ * is constructed in exactly one place, inside `DatabaseClient`, so no caller holds a raw handle and no database ends up
25
+ * described by two schemas.
26
+ */
27
+ export async function openBuiltClient<DB = Database>(
28
+ path: string,
29
+ opts: { write?: boolean } = {}
30
+ ): Promise<DatabaseClient<DB>> {
31
+ if (opts.write) {
32
+ await assertUnsealedForWrite(path)
33
+
34
+ return new DatabaseClient<DB>(path)
35
+ }
36
+
37
+ return new DatabaseClient<DB>(path, { readOnly: true })
38
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Batched bulk-write transactions — the `COMMIT`/`BEGIN` cadence every streaming ingest repeats around its
7
+ * raw prepared-statement loop.
8
+ */
9
+ /**
10
+ * One open batched transaction over a connection.
11
+ */
12
+ export interface BatchedTransaction {
13
+ /**
14
+ * Record one written row. Every `rowsPerCommit` rows the open transaction is committed and a new one opened; answers
15
+ * `true` on the call that committed, so a caller can hang a progress report off the boundary.
16
+ */
17
+ rowWritten(): boolean;
18
+ /**
19
+ * Commit the open transaction — the ordinary end of an ingest loop.
20
+ */
21
+ commit(): void;
22
+ /**
23
+ * Best-effort `ROLLBACK`, and it must never replace the real error: a build runs with the journal off (nothing is
24
+ * ever published without the swap), so SQLite may refuse to unwind. What matters is that the caller sees WHY the
25
+ * ingest stopped, not that a scratch file was tidied.
26
+ */
27
+ rollbackQuietly(): void;
28
+ }
29
+ /**
30
+ * Open a transaction that commits and reopens itself every `rowsPerCommit` written rows.
31
+ *
32
+ * The batch size is the caller's: a geometry table whose rows carry a blob wants a smaller transaction than a
33
+ * fixed-width staging table, because a larger one grows the write-ahead file without improving throughput.
34
+ */
35
+ export declare function beginBatched(database: {
36
+ exec(sql: string): void;
37
+ }, options: {
38
+ rowsPerCommit: number;
39
+ }): BatchedTransaction;
40
+ //# sourceMappingURL=batched.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batched.d.ts","sourceRoot":"","sources":["../lib/batched.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAA;IACrB;;OAEG;IACH,MAAM,IAAI,IAAI,CAAA;IACd;;;;OAIG;IACH,eAAe,IAAI,IAAI,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC3B,QAAQ,EAAE;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACrC,OAAO,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAChC,kBAAkB,CA6BpB"}
package/out/batched.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Batched bulk-write transactions — the `COMMIT`/`BEGIN` cadence every streaming ingest repeats around its
7
+ * raw prepared-statement loop.
8
+ */
9
+ /**
10
+ * Open a transaction that commits and reopens itself every `rowsPerCommit` written rows.
11
+ *
12
+ * The batch size is the caller's: a geometry table whose rows carry a blob wants a smaller transaction than a
13
+ * fixed-width staging table, because a larger one grows the write-ahead file without improving throughput.
14
+ */
15
+ export function beginBatched(database, options) {
16
+ let pending = 0;
17
+ database.exec("BEGIN");
18
+ return {
19
+ rowWritten() {
20
+ pending++;
21
+ if (pending < options.rowsPerCommit)
22
+ return false;
23
+ database.exec("COMMIT");
24
+ database.exec("BEGIN");
25
+ pending = 0;
26
+ return true;
27
+ },
28
+ commit() {
29
+ database.exec("COMMIT");
30
+ },
31
+ rollbackQuietly() {
32
+ try {
33
+ database.exec("ROLLBACK");
34
+ }
35
+ catch {
36
+ // The temp artifact is discarded either way.
37
+ }
38
+ },
39
+ };
40
+ }
41
+ //# sourceMappingURL=batched.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batched.js","sourceRoot":"","sources":["../lib/batched.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC3B,QAAqC,EACrC,OAAkC;IAElC,IAAI,OAAO,GAAG,CAAC,CAAA;IAEf,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAEtB,OAAO;QACN,UAAU;YACT,OAAO,EAAE,CAAA;YAET,IAAI,OAAO,GAAG,OAAO,CAAC,aAAa;gBAAE,OAAO,KAAK,CAAA;YAEjD,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACvB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YAEtB,OAAO,GAAG,CAAC,CAAA;YAEX,OAAO,IAAI,CAAA;QACZ,CAAC;QACD,MAAM;YACL,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACxB,CAAC;QACD,eAAe;YACd,IAAI,CAAC;gBACJ,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;YAC1B,CAAC;YAAC,MAAM,CAAC;gBACR,6CAA6C;YAC9C,CAAC;QACF,CAAC;KACD,CAAA;AACF,CAAC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+ import { DatabaseSync, type DatabaseSyncOptions, type FunctionOptions, type SQLInputValue, type SQLOutputValue, type StatementSync } from "node:sqlite";
7
+ import { Kysely, type KyselyConfig } from "kysely";
8
+ import { type PathBuilderLike } from "path-ts";
9
+ import type { Database } from "#database-schema";
10
+ import type { SqliteDialectConfig } from "#dialect-config";
11
+ /**
12
+ * A SQLite client for one database file: a Kysely query builder over `node:sqlite`, plus the two raw statements Kysely
13
+ * cannot express.
14
+ *
15
+ * The client opens the file. A caller says which file and which schema; it never builds the connection itself.
16
+ *
17
+ * ```ts
18
+ * using kdb = new DatabaseClient<MySchema>("db.sqlite", { readOnly: true })
19
+ * ```
20
+ *
21
+ * Handing that job to callers is what let one database be described by two schemas — the type argument here and a
22
+ * second one on the handle — with nothing to make them agree. It also split ownership: `SqliteDriver.destroy()` closes
23
+ * whatever connection it was given, so a shared handle has two owners and the first `destroy()` closes it under the
24
+ * other. One construction gives one schema, one owner, and a lifetime `using` can end.
25
+ *
26
+ * `exec` and `prepare` reach the same connection for the work Kysely does not model. See `core/db/schema.ts`.
27
+ */
28
+ /**
29
+ * A connection's non-Kysely surface: the statements Kysely does not model, plus ending the connection.
30
+ *
31
+ * Schema-agnostic by construction — none of these members mentions `DB` — so a `DatabaseClient<AnySchema>` satisfies it
32
+ * and a helper need not name its caller's schema. That matters because Kysely is INVARIANT in `DB`: a parameter typed
33
+ * `DatabaseClient` (the empty default) would reject every real client. This is the narrowing the
34
+ * `mailwoman/no-database-handle-cast` rule prescribes, applied to the raw side.
35
+ */
36
+ export type RawStatements = Pick<DatabaseClient, "exec" | "prepare" | "function" | "destroy"> & Disposable;
37
+ export declare class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposable {
38
+ #private;
39
+ /**
40
+ * A memory-only database, for tests and other ephemeral work. The connection is open until the client is disposed.
41
+ */
42
+ static temp<DB = Database>(): DatabaseClient<DB>;
43
+ constructor(location: PathBuilderLike, options?: DatabaseSyncOptions, config?: Partial<KyselyConfig>);
44
+ constructor(database: DatabaseSync, config?: Partial<KyselyConfig>);
45
+ constructor(dialectConfig: SqliteDialectConfig, config?: Partial<KyselyConfig>);
46
+ /**
47
+ * Run a statement Kysely does not model: `PRAGMA`, `VACUUM`, `ANALYZE`, `ATTACH`, FTS5 virtual-table DDL.
48
+ *
49
+ * Not to be confused with Kysely's `execute()`, which runs a compiled query. This one takes SQL text and returns
50
+ * nothing.
51
+ */
52
+ exec(sql: string): void;
53
+ /**
54
+ * A prepared statement on this client's connection, for the bulk-write path.
55
+ *
56
+ * Kysely compiles per call, which a positional insert loop over millions of rows cannot afford. Reach for this only
57
+ * there and in the other cases `AGENTS.md` lists as deliberately raw; everything cold goes through the query builder,
58
+ * where the schema is checked.
59
+ */
60
+ prepare(sql: string): StatementSync;
61
+ /**
62
+ * Register a user-defined SQL function on this client's connection, callable from any statement it runs.
63
+ *
64
+ * SQLite resolves the name at statement-compile time, so registration must precede the first query that uses it.
65
+ */
66
+ function(name: string, options: FunctionOptions, fn: (...args: SQLOutputValue[]) => SQLInputValue): void;
67
+ function(name: string, fn: (...args: SQLOutputValue[]) => SQLInputValue): void;
68
+ /**
69
+ * End the connection at scope exit, synchronously.
70
+ *
71
+ * `destroy()` is Kysely's teardown and returns a promise, which `Symbol.dispose` cannot await — calling it here would
72
+ * return with the file still open, and the next thing to touch that path (a `sealDatabase`, a reader expecting a
73
+ * finalized statement) would see a connection that should have been gone. `node:sqlite`'s `close()` is synchronous,
74
+ * so `using` closes the file before the scope ends.
75
+ *
76
+ * Kysely's own driver state is not unwound here. It holds this one connection and nothing else, and a query issued
77
+ * afterwards fails on the closed handle rather than on a pool that thinks it is alive.
78
+ */
79
+ [Symbol.dispose](): void;
80
+ }
81
+ /**
82
+ * The `node:sqlite` types a caller needs when it holds a statement or binds a value. Re-exported so nothing has to
83
+ * reach past this package for them.
84
+ */
85
+ export type { DatabaseSyncOptions, SQLInputValue, SQLOutputValue, StatementSync } from "node:sqlite";
86
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../lib/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAe,KAAK,eAAe,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAEhD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAE1D;;;;;;;;;;;;;;;;GAgBG;AACH;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC,GAAG,UAAU,CAAA;AAE1G,qBAAa,cAAc,CAAC,EAAE,GAAG,QAAQ,CAAE,SAAQ,MAAM,CAAC,EAAE,CAAE,YAAW,UAAU;;IAClF;;OAEG;WACW,IAAI,CAAC,EAAE,GAAG,QAAQ,KAAK,cAAc,CAAC,EAAE,CAAC;gBAM3C,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;gBACxF,QAAQ,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;gBACtD,aAAa,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;IAuC9E;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIvB;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa;IAInC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,cAAc,EAAE,KAAK,aAAa,GAAG,IAAI;IACxG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,cAAc,EAAE,KAAK,aAAa,GAAG,IAAI;IAe9E;;;;;;;;;;OAUG;IACH,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAGxB;AAED;;;GAGG;AACH,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
package/out/client.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+ import { DatabaseSync, } from "node:sqlite";
7
+ import { Kysely } from "kysely";
8
+ import { PathBuilder } from "path-ts";
9
+ import { SqliteDialect } from "#dialect";
10
+ export class DatabaseClient extends Kysely {
11
+ /**
12
+ * A memory-only database, for tests and other ephemeral work. The connection is open until the client is disposed.
13
+ */
14
+ static temp() {
15
+ return new DatabaseClient(":memory:");
16
+ }
17
+ #database;
18
+ constructor(source, optionsOrConfig, config) {
19
+ let database;
20
+ let kyselyConfig;
21
+ if (typeof source === "string" || source instanceof PathBuilder) {
22
+ const location = source.toString();
23
+ const options = optionsOrConfig;
24
+ // node:sqlite checks the argument count, not the value: `new DatabaseSync(path, undefined)` throws.
25
+ const openArgs = options ? [location, options] : [location];
26
+ database = new DatabaseSync(...openArgs);
27
+ kyselyConfig = config;
28
+ }
29
+ else {
30
+ const dialectConfig = "database" in source ? source : { database: source };
31
+ if (typeof dialectConfig.database === "function") {
32
+ throw new TypeError("DatabaseClient needs an open connection: a lazy `() => Promise<DatabaseSync>` leaves `exec` and " +
33
+ "`prepare` with nothing to reach. Pass the database path instead.");
34
+ }
35
+ database = dialectConfig.database;
36
+ kyselyConfig = optionsOrConfig;
37
+ }
38
+ super({
39
+ ...kyselyConfig,
40
+ dialect: new SqliteDialect({ database }),
41
+ });
42
+ this.#database = database;
43
+ }
44
+ /**
45
+ * Run a statement Kysely does not model: `PRAGMA`, `VACUUM`, `ANALYZE`, `ATTACH`, FTS5 virtual-table DDL.
46
+ *
47
+ * Not to be confused with Kysely's `execute()`, which runs a compiled query. This one takes SQL text and returns
48
+ * nothing.
49
+ */
50
+ exec(sql) {
51
+ this.#database.exec(sql);
52
+ }
53
+ /**
54
+ * A prepared statement on this client's connection, for the bulk-write path.
55
+ *
56
+ * Kysely compiles per call, which a positional insert loop over millions of rows cannot afford. Reach for this only
57
+ * there and in the other cases `AGENTS.md` lists as deliberately raw; everything cold goes through the query builder,
58
+ * where the schema is checked.
59
+ */
60
+ prepare(sql) {
61
+ return this.#database.prepare(sql);
62
+ }
63
+ function(name, optionsOrFn, fn) {
64
+ if (typeof optionsOrFn === "function") {
65
+ this.#database.function(name, optionsOrFn);
66
+ return;
67
+ }
68
+ this.#database.function(name, optionsOrFn, fn);
69
+ }
70
+ /**
71
+ * End the connection at scope exit, synchronously.
72
+ *
73
+ * `destroy()` is Kysely's teardown and returns a promise, which `Symbol.dispose` cannot await — calling it here would
74
+ * return with the file still open, and the next thing to touch that path (a `sealDatabase`, a reader expecting a
75
+ * finalized statement) would see a connection that should have been gone. `node:sqlite`'s `close()` is synchronous,
76
+ * so `using` closes the file before the scope ends.
77
+ *
78
+ * Kysely's own driver state is not unwound here. It holds this one connection and nothing else, and a query issued
79
+ * afterwards fails on the closed handle rather than on a pool that thinks it is alive.
80
+ */
81
+ [Symbol.dispose]() {
82
+ this.#database[Symbol.dispose]();
83
+ }
84
+ }
85
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../lib/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,YAAY,GAMZ,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,MAAM,EAAqB,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,WAAW,EAAwB,MAAM,SAAS,CAAA;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AA8BxC,MAAM,OAAO,cAA8B,SAAQ,MAAU;IAC5D;;OAEG;IACI,MAAM,CAAC,IAAI;QACjB,OAAO,IAAI,cAAc,CAAK,UAAU,CAAC,CAAA;IAC1C,CAAC;IAEQ,SAAS,CAAc;IAKhC,YACC,MAA4D,EAC5D,eAA6D,EAC7D,MAA8B;QAE9B,IAAI,QAAsB,CAAA;QAC1B,IAAI,YAA+C,CAAA;QAEnD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,YAAY,WAAW,EAAE,CAAC;YACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;YAClC,MAAM,OAAO,GAAG,eAAkD,CAAA;YAClE,oGAAoG;YACpG,MAAM,QAAQ,GAAmC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;YAE3F,QAAQ,GAAG,IAAI,YAAY,CAAC,GAAG,QAAQ,CAAC,CAAA;YACxC,YAAY,GAAG,MAAM,CAAA;QACtB,CAAC;aAAM,CAAC;YACP,MAAM,aAAa,GAAG,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;YAE1E,IAAI,OAAO,aAAa,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAClD,MAAM,IAAI,SAAS,CAClB,kGAAkG;oBACjG,kEAAkE,CACnE,CAAA;YACF,CAAC;YAED,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAA;YACjC,YAAY,GAAG,eAAoD,CAAA;QACpE,CAAC;QAED,KAAK,CAAC;YACL,GAAG,YAAY;YACf,OAAO,EAAE,IAAI,aAAa,CAAC,EAAE,QAAQ,EAAE,CAAC;SACxC,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,GAAW;QACf,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,GAAW;QAClB,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACnC,CAAC;IASD,QAAQ,CACP,IAAY,EACZ,WAA6E,EAC7E,EAAiD;QAEjD,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;YACvC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;YAE1C,OAAM;QACP,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,EAAG,CAAC,CAAA;IAChD,CAAC;IAED;;;;;;;;;;OAUG;IACH,CAAC,MAAM,CAAC,OAAO,CAAC;QACf,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;IACjC,CAAC;CACD"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Shared `Database` typing for the Kysely client at `core/kysley/client.ts`.
7
+ *
8
+ * This file intentionally ships an empty schema. Consumers that want compile-time table typing
9
+ * should extend `Kysely<TheirSchema>` directly rather than baking a schema into core — the WOF
10
+ * resolver (Phase 4.2), corpus adapters, and any future consumer all touch different tables.
11
+ *
12
+ * The exported `Database` interface exists so `client.ts` has a target for `Kysely<Database>` and
13
+ * the boilerplate compiles without forcing a concrete schema commitment.
14
+ *
15
+ * IT IS THE DEFAULT, NOT THE ONLY OPTION, and reading it as the only option is how `as never` gets written. Every
16
+ * table name typed against `Record<string, never>` IS `never`, so `insertInto("poi")` cannot compile and a caller
17
+ * who believes no alternative exists casts — which disarms not just that one check but every type-level guarantee
18
+ * anyone later adds to the table (#1757 has the worked case: a branded key column whose check a neighbouring
19
+ * `as never` had already defeated).
20
+ */
21
+ /**
22
+ * Empty schema marker, and the DEFAULT type argument only.
23
+ *
24
+ * Consumers declare their own schema interface and pass it directly: `new Kysely<MySchema>({…})` or `new
25
+ * DatabaseClient<MySchema>(…)`. `DatabaseClient<DB = Database>` is generic today, so this is available now — the
26
+ * sentence here once deferred it to "Phase 4.2 work", which had already landed, and a reader who believed that had no
27
+ * option but to cast.
28
+ */
29
+ export type Database = Record<string, never>;
30
+ //# sourceMappingURL=database-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-schema.d.ts","sourceRoot":"","sources":["../lib/database-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Shared `Database` typing for the Kysely client at `core/kysley/client.ts`.
7
+ *
8
+ * This file intentionally ships an empty schema. Consumers that want compile-time table typing
9
+ * should extend `Kysely<TheirSchema>` directly rather than baking a schema into core — the WOF
10
+ * resolver (Phase 4.2), corpus adapters, and any future consumer all touch different tables.
11
+ *
12
+ * The exported `Database` interface exists so `client.ts` has a target for `Kysely<Database>` and
13
+ * the boilerplate compiles without forcing a concrete schema commitment.
14
+ *
15
+ * IT IS THE DEFAULT, NOT THE ONLY OPTION, and reading it as the only option is how `as never` gets written. Every
16
+ * table name typed against `Record<string, never>` IS `never`, so `insertInto("poi")` cannot compile and a caller
17
+ * who believes no alternative exists casts — which disarms not just that one check but every type-level guarantee
18
+ * anyone later adds to the table (#1757 has the worked case: a branded key column whose check a neighbouring
19
+ * `as never` had already defeated).
20
+ */
21
+ export {};
22
+ //# sourceMappingURL=database-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-schema.js","sourceRoot":"","sources":["../lib/database-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}