@mailwoman/sqlite 9.4.0 → 10.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.
Files changed (74) hide show
  1. package/lib/batched.ts +7 -13
  2. package/lib/client.ts +15 -42
  3. package/lib/database-schema.ts +2 -20
  4. package/lib/dialect.ts +1 -1
  5. package/lib/driver.ts +3 -3
  6. package/lib/index.ts +7 -6
  7. package/lib/introspection.ts +15 -8
  8. package/lib/lookup.ts +77 -0
  9. package/lib/schema-columns.ts +18 -19
  10. package/lib/{sealed-build.ts → sealed/build.ts} +15 -26
  11. package/lib/sealed/db.ts +191 -0
  12. package/lib/sealed.ts +14 -9
  13. package/out/batched.d.ts +7 -13
  14. package/out/batched.d.ts.map +1 -1
  15. package/out/batched.js +3 -5
  16. package/out/batched.js.map +1 -1
  17. package/out/client.d.ts +14 -41
  18. package/out/client.d.ts.map +1 -1
  19. package/out/client.js +9 -17
  20. package/out/client.js.map +1 -1
  21. package/out/database-schema.d.ts +2 -20
  22. package/out/database-schema.d.ts.map +1 -1
  23. package/out/database-schema.js +1 -14
  24. package/out/database-schema.js.map +1 -1
  25. package/out/{dialect-config.d.ts → dialect/config.d.ts} +1 -1
  26. package/out/dialect/config.d.ts.map +1 -0
  27. package/out/{dialect-config.js → dialect/config.js} +1 -1
  28. package/out/dialect/config.js.map +1 -0
  29. package/out/dialect.d.ts +1 -1
  30. package/out/dialect.d.ts.map +1 -1
  31. package/out/driver.d.ts +1 -1
  32. package/out/driver.d.ts.map +1 -1
  33. package/out/driver.js +2 -2
  34. package/out/driver.js.map +1 -1
  35. package/out/index.d.ts +7 -6
  36. package/out/index.d.ts.map +1 -1
  37. package/out/index.js +7 -6
  38. package/out/index.js.map +1 -1
  39. package/out/introspection.d.ts +15 -8
  40. package/out/introspection.d.ts.map +1 -1
  41. package/out/introspection.js +15 -8
  42. package/out/introspection.js.map +1 -1
  43. package/out/lookup.d.ts +49 -0
  44. package/out/lookup.d.ts.map +1 -0
  45. package/out/lookup.js +55 -0
  46. package/out/lookup.js.map +1 -0
  47. package/out/schema-columns.d.ts +18 -19
  48. package/out/schema-columns.d.ts.map +1 -1
  49. package/out/schema-columns.js +14 -17
  50. package/out/schema-columns.js.map +1 -1
  51. package/out/sealed/build.d.ts +40 -0
  52. package/out/sealed/build.d.ts.map +1 -0
  53. package/out/{sealed-build.js → sealed/build.js} +3 -16
  54. package/out/sealed/build.js.map +1 -0
  55. package/out/sealed/db.d.ts +90 -0
  56. package/out/sealed/db.d.ts.map +1 -0
  57. package/out/{sealed-db.js → sealed/db.js} +63 -43
  58. package/out/sealed/db.js.map +1 -0
  59. package/out/sealed.d.ts +12 -8
  60. package/out/sealed.d.ts.map +1 -1
  61. package/out/sealed.js +11 -8
  62. package/out/sealed.js.map +1 -1
  63. package/out/tsconfig.test.tsbuildinfo +1 -1
  64. package/package.json +15 -104
  65. package/lib/sealed-db.ts +0 -170
  66. package/out/dialect-config.d.ts.map +0 -1
  67. package/out/dialect-config.js.map +0 -1
  68. package/out/sealed-build.d.ts +0 -51
  69. package/out/sealed-build.d.ts.map +0 -1
  70. package/out/sealed-build.js.map +0 -1
  71. package/out/sealed-db.d.ts +0 -74
  72. package/out/sealed-db.d.ts.map +0 -1
  73. package/out/sealed-db.js.map +0 -1
  74. /package/lib/{dialect-config.ts → dialect/config.ts} +0 -0
package/lib/batched.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Batched bulk-write transactions — the `COMMIT`/`BEGIN` cadence every streaming ingest repeats around its
6
+ * Batched bulk-write transactions — the `commit`/`begin` cadence every streaming ingest repeats around its
7
7
  * raw prepared-statement loop.
8
8
  */
9
9
 
@@ -12,27 +12,21 @@
12
12
  */
13
13
  export interface BatchedTransaction {
14
14
  /**
15
- * Record one written row. Every `rowsPerCommit` rows the open transaction is committed and a new one opened; answers
16
- * `true` on the call that committed, so a caller can hang a progress report off the boundary.
15
+ * Record one written row, committing and reopening the transaction every `rowsPerCommit`
16
+ * rows and answering `true` on the call that committed.
17
17
  */
18
18
  rowWritten(): boolean
19
- /**
20
- * Commit the open transaction — the ordinary end of an ingest loop.
21
- */
22
19
  commit(): void
23
20
  /**
24
- * Best-effort `ROLLBACK`, and it must never replace the real error: a build runs with the journal off (nothing is
25
- * ever published without the swap), so SQLite may refuse to unwind. What matters is that the caller sees WHY the
26
- * ingest stopped, not that a scratch file was tidied.
21
+ * Best-effort `rollback` that must never replace the real error, so the caller
22
+ * still sees why the ingest stopped.
27
23
  */
28
24
  rollbackQuietly(): void
29
25
  }
30
26
 
31
27
  /**
32
- * Open a transaction that commits and reopens itself every `rowsPerCommit` written rows.
33
- *
34
- * The batch size is the caller's: a geometry table whose rows carry a blob wants a smaller transaction than a
35
- * fixed-width staging table, because a larger one grows the write-ahead file without improving throughput.
28
+ * Open a transaction that commits and reopens itself every `rowsPerCommit` written rows,
29
+ * with the batch size left to the caller.
36
30
  */
37
31
  export function beginBatched(
38
32
  database: { exec(sql: string): void },
package/lib/client.ts CHANGED
@@ -18,38 +18,23 @@ import { PathBuilder, type PathBuilderLike } from "path-ts"
18
18
 
19
19
  import type { Database } from "#database-schema"
20
20
  import { SqliteDialect } from "#dialect"
21
- import type { SqliteDialectConfig } from "#dialect-config"
21
+ import type { SqliteDialectConfig } from "#dialect/config"
22
22
 
23
- /**
24
- * A SQLite client for one database file: a Kysely query builder over `node:sqlite`, plus the two raw statements Kysely
25
- * cannot express.
26
- *
27
- * The client opens the file. A caller says which file and which schema; it never builds the connection itself.
28
- *
29
- * ```ts
30
- * using kdb = new DatabaseClient<MySchema>("db.sqlite", { readOnly: true })
31
- * ```
32
- *
33
- * Handing that job to callers is what let one database be described by two schemas — the type argument here and a
34
- * second one on the handle — with nothing to make them agree. It also split ownership: `SqliteDriver.destroy()` closes
35
- * whatever connection it was given, so a shared handle has two owners and the first `destroy()` closes it under the
36
- * other. One construction gives one schema, one owner, and a lifetime `using` can end.
37
- *
38
- * `exec` and `prepare` reach the same connection for the work Kysely does not model. See `core/db/schema.ts`.
39
- */
40
23
  /**
41
24
  * A connection's non-Kysely surface: the statements Kysely does not model, plus ending the connection.
42
25
  *
43
- * Schema-agnostic by construction — none of these members mentions `DB` — so a `DatabaseClient<AnySchema>` satisfies it
44
- * and a helper need not name its caller's schema. That matters because Kysely is INVARIANT in `DB`: a parameter typed
45
- * `DatabaseClient` (the empty default) would reject every real client. This is the narrowing the
46
- * `mailwoman/no-database-handle-cast` rule prescribes, applied to the raw side.
26
+ * Schema-agnostic because Kysely is invariant in `DB`: a parameter typed
27
+ * `DatabaseClient` would reject every real client.
47
28
  */
48
29
  export type RawStatements = Pick<DatabaseClient, "exec" | "prepare" | "function" | "destroy"> & Disposable
49
30
 
31
+ /**
32
+ * A SQLite client for one database file: a Kysely query builder over `node:sqlite`,
33
+ * plus the two raw statements Kysely cannot express.
34
+ */
50
35
  export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposable {
51
36
  /**
52
- * A memory-only database, for tests and other ephemeral work. The connection is open until the client is disposed.
37
+ * A memory-only database, for tests and other ephemeral work.
53
38
  */
54
39
  public static temp<DB = Database>(): DatabaseClient<DB> {
55
40
  return new DatabaseClient<DB>(":memory:")
@@ -71,7 +56,7 @@ export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposa
71
56
  if (typeof source === "string" || source instanceof PathBuilder) {
72
57
  const location = source.toString()
73
58
  const options = optionsOrConfig as DatabaseSyncOptions | undefined
74
- // node:sqlite checks the argument count, not the value: `new DatabaseSync(path, undefined)` throws.
59
+ // node:sqlite checks the argument count rather than the value: `new DatabaseSync(path, undefined)` throws.
75
60
  const openArgs: [string, DatabaseSyncOptions?] = options ? [location, options] : [location]
76
61
 
77
62
  database = new DatabaseSync(...openArgs)
@@ -99,10 +84,7 @@ export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposa
99
84
  }
100
85
 
101
86
  /**
102
- * Run a statement Kysely does not model: `PRAGMA`, `VACUUM`, `ANALYZE`, `ATTACH`, FTS5 virtual-table DDL.
103
- *
104
- * Not to be confused with Kysely's `execute()`, which runs a compiled query. This one takes SQL text and returns
105
- * nothing.
87
+ * Run a statement Kysely does not model: `pragma`, `vacuum`, `analyze`, `attach`, FTS5 virtual-table DDL.
106
88
  */
107
89
  exec(sql: string): void {
108
90
  this.#database.exec(sql)
@@ -110,10 +92,6 @@ export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposa
110
92
 
111
93
  /**
112
94
  * A prepared statement on this client's connection, for the bulk-write path.
113
- *
114
- * Kysely compiles per call, which a positional insert loop over millions of rows cannot afford. Reach for this only
115
- * there and in the other cases `AGENTS.md` lists as deliberately raw; everything cold goes through the query builder,
116
- * where the schema is checked.
117
95
  */
118
96
  prepare(sql: string): StatementSync {
119
97
  return this.#database.prepare(sql)
@@ -122,7 +100,8 @@ export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposa
122
100
  /**
123
101
  * Register a user-defined SQL function on this client's connection, callable from any statement it runs.
124
102
  *
125
- * SQLite resolves the name at statement-compile time, so registration must precede the first query that uses it.
103
+ * SQLite resolves the name at statement-compile time, so registration must
104
+ * precede the first query that uses it.
126
105
  */
127
106
  function(name: string, options: FunctionOptions, fn: (...args: SQLOutputValue[]) => SQLInputValue): void
128
107
  function(name: string, fn: (...args: SQLOutputValue[]) => SQLInputValue): void
@@ -143,13 +122,8 @@ export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposa
143
122
  /**
144
123
  * End the connection at scope exit, synchronously.
145
124
  *
146
- * `destroy()` is Kysely's teardown and returns a promise, which `Symbol.dispose` cannot await — calling it here would
147
- * return with the file still open, and the next thing to touch that path (a `sealDatabase`, a reader expecting a
148
- * finalized statement) would see a connection that should have been gone. `node:sqlite`'s `close()` is synchronous,
149
- * so `using` closes the file before the scope ends.
150
- *
151
- * Kysely's own driver state is not unwound here. It holds this one connection and nothing else, and a query issued
152
- * afterwards fails on the closed handle rather than on a pool that thinks it is alive.
125
+ * `destroy()` returns a promise, so `Symbol.dispose` cannot call it.
126
+ * `node:sqlite`'s `close()` is synchronous.
153
127
  */
154
128
  [Symbol.dispose](): void {
155
129
  this.#database[Symbol.dispose]()
@@ -157,7 +131,6 @@ export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposa
157
131
  }
158
132
 
159
133
  /**
160
- * The `node:sqlite` types a caller needs when it holds a statement or binds a value. Re-exported so nothing has to
161
- * reach past this package for them.
134
+ * The `node:sqlite` types a caller needs when it holds a statement or binds a value.
162
135
  */
163
136
  export type { DatabaseSyncOptions, SQLInputValue, SQLOutputValue, StatementSync } from "node:sqlite"
@@ -3,28 +3,10 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
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).
6
+ * Default schema type for {@link DatabaseClient}; consumers supply their own table schema.
20
7
  */
21
8
 
22
9
  /**
23
- * Empty schema marker, and the DEFAULT type argument only.
24
- *
25
- * Consumers declare their own schema interface and pass it directly: `new Kysely<MySchema>({…})` or `new
26
- * DatabaseClient<MySchema>(…)`. `DatabaseClient<DB = Database>` is generic today, so this is available now — the
27
- * sentence here once deferred it to "Phase 4.2 work", which had already landed, and a reader who believed that had no
28
- * option but to cast.
10
+ * Empty schema marker used only as the generic default.
29
11
  */
30
12
  export type Database = Record<string, never>
package/lib/dialect.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  type QueryCompiler,
17
17
  } from "kysely"
18
18
 
19
- import type { SqliteDialectConfig } from "#dialect-config"
19
+ import type { SqliteDialectConfig } from "#dialect/config"
20
20
  import { SqliteDriver } from "#driver"
21
21
 
22
22
  /**
package/lib/driver.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  SelectQueryNode,
19
19
  } from "kysely"
20
20
 
21
- import type { SqliteDialectConfig } from "#dialect-config"
21
+ import type { SqliteDialectConfig } from "#dialect/config"
22
22
 
23
23
  class ConnectionMutex {
24
24
  #promise?: Promise<void>
@@ -67,8 +67,8 @@ export class SqliteDriver implements Driver {
67
67
  }
68
68
 
69
69
  async acquireConnection(): Promise<DatabaseConnection> {
70
- // SQLite only has one single connection. We use a mutex here to wait
71
- // until the single connection has been released.
70
+ // SQLite only has one single connection.
71
+ // We use a mutex here to wait until the single connection has been released.
72
72
  await this.#connectionMutex.lock()
73
73
 
74
74
  // biome-ignore lint/style/noNonNullAssertion: :shrug:
package/lib/index.ts CHANGED
@@ -5,11 +5,11 @@
5
5
  *
6
6
  * The one way a Mailwoman SQLite connection comes into being.
7
7
  *
8
- * `DatabaseClient` opens the file; `openBuiltClient` opens a built artifact with the seal check. A caller says which
9
- * file and which schema and never constructs the connection, so one database carries one schema, one owner, and a
8
+ * `DatabaseClient` opens the file; `openBuiltClient` opens a built artifact with the seal check. A caller specifies which
9
+ * file and schema. It never constructs the connection, so one database has one schema, one owner and a
10
10
  * lifetime `using` can end. `new DatabaseSync` appears once in this package and nowhere else in the repository.
11
11
  *
12
- * It lives outside `@mailwoman/core` because it needs none of core's 11 MB of parser reference data — its whole
12
+ * It lives outside `@mailwoman/core` because it needs none of core's 11 MB of parser reference data. Its whole
13
13
  * dependency set is `kysely`. That is what lets the leaf lookups use it.
14
14
  */
15
15
 
@@ -17,9 +17,10 @@ export * from "#batched"
17
17
  export * from "#client"
18
18
  export * from "#database-schema"
19
19
  export * from "#dialect"
20
- export * from "#dialect-config"
20
+ export * from "#dialect/config"
21
21
  export * from "#introspection"
22
+ export * from "#lookup"
22
23
  export * from "#schema-columns"
23
24
  export * from "#sealed"
24
- export * from "#sealed-build"
25
- export * from "#sealed-db"
25
+ export * from "#sealed/build"
26
+ export * from "#sealed/db"
@@ -4,8 +4,8 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  * @file Questions asked of `sqlite_master`.
6
6
  *
7
- * ITS OWN SUBPATH ON PURPOSE — do not fold this into the package barrel. `@mailwoman/sqlite`'s root export
8
- * reaches `node:sqlite`, and the docs demo bundles this package for the browser; importing the barrel to
7
+ * its own subpath on purpose — do not fold this into the package barrel. `@mailwoman/sqlite`'s root export
8
+ * reaches `node:sqlite`, and the docs demo bundles this package for the browser. importing the barrel to
9
9
  * ask whether a table exists dragged the builtin into that graph. The split is what keeps these three
10
10
  * pure-SQL questions reachable from a bundled consumer.
11
11
  */
@@ -13,14 +13,18 @@
13
13
  import type { DatabaseClient } from "#client"
14
14
 
15
15
  /**
16
- * Whether a TABLE named `name` exists. An index, view or trigger of that name answers `false`.
16
+ * Whether a table exists with the value in `name`.
17
+ *
18
+ * An index, view or trigger of that name answers `false`.
17
19
  */
18
20
  export function tableExists<DB>(db: DatabaseClient<DB>, name: string): boolean {
19
21
  return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name))
20
22
  }
21
23
 
22
24
  /**
23
- * `COUNT(*)` over `table`. The name is interpolated, so it must come from the schema, never from input.
25
+ * `count(*)` over `table`.
26
+ *
27
+ * The name is interpolated, so it must come from the schema, never from input.
24
28
  */
25
29
  export function countRows<DB>(db: DatabaseClient<DB>, table: string): number {
26
30
  const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n?: number } | undefined
@@ -29,14 +33,16 @@ export function countRows<DB>(db: DatabaseClient<DB>, table: string): number {
29
33
  }
30
34
 
31
35
  /**
32
- * Whether an INDEX named `name` exists.
36
+ * Whether an index exists with the value in `name`.
33
37
  */
34
38
  export function indexExists<DB>(db: DatabaseClient<DB>, name: string): boolean {
35
39
  return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?").get(name))
36
40
  }
37
41
 
38
42
  /**
39
- * Every table name, sorted. Virtual tables are tables here, the way `sqlite_master` records them.
43
+ * Every table name, sorted.
44
+ *
45
+ * Virtual tables are tables here, the way `sqlite_master` records them.
40
46
  */
41
47
  export function listTables<DB>(db: DatabaseClient<DB>): string[] {
42
48
  const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all() as Array<{
@@ -47,8 +53,9 @@ export function listTables<DB>(db: DatabaseClient<DB>): string[] {
47
53
  }
48
54
 
49
55
  /**
50
- * Whether `table` has a column named `column`. The table name is spliced into a PRAGMA, so it must come from the
51
- * schema, never from input.
56
+ * Whether `table` has a column with the value in `column`.
57
+ *
58
+ * The table name is spliced into a pragma, so it must come from the schema, never from input.
52
59
  */
53
60
  export function hasColumn<DB>(db: DatabaseClient<DB>, table: string, column: string): boolean {
54
61
  const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>
package/lib/lookup.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The shared shape of a reader over one SQLite artifact.
7
+ * A lookup either opens the file it is given or adopts a connection already held by the caller.
8
+ * and it closes on disposal only what it opened.
9
+ */
10
+
11
+ import type { PathBuilderLike } from "path-ts"
12
+
13
+ import { DatabaseClient, type DatabaseSyncOptions } from "#client"
14
+
15
+ /**
16
+ * Where a lookup's connection comes from.
17
+ * Exactly one of the two fields is set.
18
+ */
19
+ export interface SQLiteLookupOptions<DB> {
20
+ /**
21
+ * The database file to open.
22
+ * The lookup owns the connection and closes it on disposal.
23
+ */
24
+ databasePath?: PathBuilderLike
25
+ /**
26
+ * A connection the caller opened.
27
+ * The lookup reads through it and leaves it open on disposal.
28
+ */
29
+ database?: DatabaseClient<DB>
30
+ }
31
+
32
+ /**
33
+ * A reader over one SQLite database that owns only the connection it opened.
34
+ *
35
+ * A subclass passes its options to `super` and reads through `this.database`.
36
+ * It inherits the disposal that closes an opened file and leaves an adopted one unchanged.
37
+ *
38
+ * The connection is opened read-only unless the subclass passes its own open options.
39
+ */
40
+ export abstract class SQLiteLookup<DB> implements Disposable {
41
+ readonly #database: DatabaseClient<DB>
42
+ /**
43
+ * The connections this instance opened.
44
+ *
45
+ * An adopted connection is never a member, so disposal cannot reach it.
46
+ */
47
+ readonly #owned = new DisposableStack()
48
+
49
+ protected constructor(source: SQLiteLookupOptions<DB>, openOptions: DatabaseSyncOptions = { readOnly: true }) {
50
+ if (source.database && source.databasePath) {
51
+ throw new Error(`${new.target.name}: pass either \`database\` or \`databasePath\`, not both`)
52
+ }
53
+
54
+ if (source.database) {
55
+ this.#database = source.database
56
+ } else if (source.databasePath) {
57
+ this.#database = this.#owned.use(new DatabaseClient<DB>(source.databasePath, openOptions))
58
+ } else {
59
+ throw new Error(`${new.target.name}: one of \`database\` or \`databasePath\` is required`)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The connection this lookup reads through.
65
+ */
66
+ protected get database(): DatabaseClient<DB> {
67
+ return this.#database
68
+ }
69
+
70
+ /**
71
+ * Close the connection this lookup opened.
72
+ * An adopted connection stays open for its owner.
73
+ */
74
+ [Symbol.dispose](): void {
75
+ this.#owned[Symbol.dispose]()
76
+ }
77
+ }
@@ -2,22 +2,21 @@
2
2
  * @copyright Sister Software
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
- *
6
- * The column groups every polygon layer's schema repeats, as schema-builder helpers — so the
7
- * `WITHOUT ROWID`-versus-blob discipline is stated once, beside the columns it governs.
8
5
  */
9
6
 
10
7
  import type { CreateTableBuilder, Kysely } from "kysely"
11
8
 
12
9
  /**
13
- * The slice of a Kysely handle a schema module's DDL touches. Kysely is invariant in its schema parameter, so naming
14
- * only the member the builders call lets a caller pass its own wider handle.
10
+ * The subset of a Kysely handle a schema module's DDL touches.
11
+ *
12
+ * Kysely is invariant in its schema parameter, so naming only this member lets
13
+ * a caller pass its own wider handle.
15
14
  */
16
15
  export type SchemaHandle<DB> = Pick<Kysely<DB>, "schema">
17
16
 
18
17
  /**
19
- * The precomputed bounding box every polygon table carries — truth tables and extent tables alike — in the order the
20
- * sealed artifacts store it.
18
+ * The precomputed bounding box every polygon table includes — truth tables
19
+ * and extent tables alike — in the order the sealed artifacts store it.
21
20
  */
22
21
  export function addBoundingBoxColumns<TB extends string, C extends string>(
23
22
  builder: CreateTableBuilder<TB, C>
@@ -30,12 +29,12 @@ export function addBoundingBoxColumns<TB extends string, C extends string>(
30
29
  }
31
30
 
32
31
  /**
33
- * The unsimplified ring blob a polygon truth table carries, kept apart from {@link addBoundingBoxColumns} because one
34
- * layer stores its own ring-derived columns between the box and the blob, and the stored column order of a sealed
35
- * artifact is part of what its readers see.
32
+ * The unsimplified ring blob a polygon truth table stores, separate from
33
+ * {@link addBoundingBoxColumns} because one layer's stored column order between the box
34
+ * and the blob is part of what sealed-artifact readers see.
36
35
  *
37
- * A table taking this stays a PLAIN rowid table, never `WITHOUT ROWID`: the `rings` blob is exactly the payload
38
- * clustering into the B-tree penalizes — every index page becomes a geometry page.
36
+ * A table taking this stays a plain rowid table, never `without rowid`: the `rings`
37
+ * blob is exactly the payload clustering into the B-tree penalizes.
39
38
  */
40
39
  export function addRingsColumn<TB extends string, C extends string>(
41
40
  builder: CreateTableBuilder<TB, C>
@@ -44,8 +43,8 @@ export function addRingsColumn<TB extends string, C extends string>(
44
43
  }
45
44
 
46
45
  /**
47
- * The precomputed bounding box plus the unsimplified ring blob every polygon truth table carries:
48
- * {@link addBoundingBoxColumns} then {@link addRingsColumn}, with nothing between them.
46
+ * The precomputed bounding box plus the unsimplified ring blob every polygon truth table includes:
47
+ * {@link addBoundingBoxColumns} then {@link addRingsColumn}, with no columns between them.
49
48
  */
50
49
  export function addRingGeometryColumns<TB extends string, C extends string>(
51
50
  builder: CreateTableBuilder<TB, C>
@@ -54,12 +53,12 @@ export function addRingGeometryColumns<TB extends string, C extends string>(
54
53
  }
55
54
 
56
55
  /**
57
- * The cell-index columns every polygon summary tier carries: the 48-bit short cell, the resolution it was captured at
58
- * (a short cell does not name its own, and a mixed-resolution table cannot be probed without it), the key columns the
59
- * row names — one, or several in the order given, for a layer whose rows are keyed per scenario — and its containment.
56
+ * The cell-index columns every polygon summary tier includes: the 48-bit short cell,
57
+ * the resolution it was captured at, the key columns the row names and its containment.
60
58
  *
61
- * Small fixed-width rows probed by their exact primary key are the `WITHOUT ROWID` shape — the caller adds its own
62
- * primary-key constraint and the raw `without rowid` modifier, because the key differs per layer.
59
+ * Small fixed-width rows probed by their exact primary key are the `without rowid`
60
+ * shape. the caller adds its own primary-key constraint and the raw `without rowid`
61
+ * modifier because the key differs per layer.
63
62
  */
64
63
  export function addCellIndexColumns<TB extends string, C extends string, K extends string>(
65
64
  builder: CreateTableBuilder<TB, C>,
@@ -2,56 +2,45 @@
2
2
  * @copyright Sister Software
3
3
  * @license AGPL-3.0
4
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
5
  */
15
6
 
16
7
  import { removePathIfPresent } from "@mailwoman/core/fs/writers"
8
+ import type { PathBuilderLike } from "path-ts"
17
9
 
18
10
  import { DatabaseClient } from "#client"
19
- import { sealDatabase, swapDatabaseIntoPlace } from "#sealed-db"
11
+ import { sealDatabase, swapDatabaseIntoPlace } from "#sealed/db"
20
12
 
21
13
  export interface BuildSealedArtifactOptions<DB, Streamed, Result> {
22
14
  /**
23
- * Where the sealed artifact lands. The build writes beside it and swaps.
15
+ * Where the sealed artifact lands.
24
16
  */
25
- out: string
17
+ out: PathBuilderLike
26
18
  /**
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).
19
+ * Create every table the build writes, including any build-only scratch table the finish phase drops.
29
20
  */
30
21
  createTables: (database: DatabaseClient<DB>) => Promise<void>
31
22
  /**
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.
23
+ * The in-process ingest, run under the first handle.
24
+ *
25
+ * Return `undefined` to defer to {@link batched}, keeping any writes made before deferring.
34
26
  */
35
27
  ingest: (database: DatabaseClient<DB>) => Promise<Streamed | undefined>
36
28
  /**
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.
29
+ * The bounded child-process ingest, run while the parent holds no handle: each child opens the
30
+ * temp file and appends one chunk at a time, so there is exactly one writer at every instant.
39
31
  */
40
32
  batched?: (tmpPath: string) => Promise<Streamed>
41
33
  /**
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.
34
+ * Post-ingest work under the second handle: assertions over what was streamed,
35
+ * index/coverage/manifest writes, dropping any scratch table.
36
+ *
37
+ * The artifact's on-disk size is measurable only after this returns and the swap lands.
45
38
  */
46
39
  finish: (database: DatabaseClient<DB>, streamed: Streamed) => Promise<Result>
47
40
  }
48
41
 
49
42
  /**
50
43
  * 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
44
  */
56
45
  export async function buildSealedArtifact<DB, Streamed, Result>(
57
46
  options: BuildSealedArtifactOptions<DB, Streamed, Result>
@@ -61,7 +50,7 @@ export async function buildSealedArtifact<DB, Streamed, Result>(
61
50
  let streamed: Streamed | undefined
62
51
 
63
52
  {
64
- await using kdb = new DatabaseClient<DB>(tmpPath)
53
+ using kdb = new DatabaseClient<DB>(tmpPath)
65
54
 
66
55
  try {
67
56
  kdb.exec("PRAGMA journal_mode = OFF")