@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.
- package/lib/batched.ts +7 -13
- package/lib/client.ts +15 -42
- package/lib/database-schema.ts +2 -20
- package/lib/dialect.ts +1 -1
- package/lib/driver.ts +3 -3
- package/lib/index.ts +7 -6
- package/lib/introspection.ts +15 -8
- package/lib/lookup.ts +77 -0
- package/lib/schema-columns.ts +18 -19
- package/lib/{sealed-build.ts → sealed/build.ts} +15 -26
- package/lib/sealed/db.ts +191 -0
- package/lib/sealed.ts +14 -9
- package/out/batched.d.ts +7 -13
- package/out/batched.d.ts.map +1 -1
- package/out/batched.js +3 -5
- package/out/batched.js.map +1 -1
- package/out/client.d.ts +14 -41
- package/out/client.d.ts.map +1 -1
- package/out/client.js +9 -17
- package/out/client.js.map +1 -1
- package/out/database-schema.d.ts +2 -20
- package/out/database-schema.d.ts.map +1 -1
- package/out/database-schema.js +1 -14
- package/out/database-schema.js.map +1 -1
- package/out/{dialect-config.d.ts → dialect/config.d.ts} +1 -1
- package/out/dialect/config.d.ts.map +1 -0
- package/out/{dialect-config.js → dialect/config.js} +1 -1
- package/out/dialect/config.js.map +1 -0
- package/out/dialect.d.ts +1 -1
- package/out/dialect.d.ts.map +1 -1
- package/out/driver.d.ts +1 -1
- package/out/driver.d.ts.map +1 -1
- package/out/driver.js +2 -2
- package/out/driver.js.map +1 -1
- package/out/index.d.ts +7 -6
- package/out/index.d.ts.map +1 -1
- package/out/index.js +7 -6
- package/out/index.js.map +1 -1
- package/out/introspection.d.ts +15 -8
- package/out/introspection.d.ts.map +1 -1
- package/out/introspection.js +15 -8
- package/out/introspection.js.map +1 -1
- package/out/lookup.d.ts +49 -0
- package/out/lookup.d.ts.map +1 -0
- package/out/lookup.js +55 -0
- package/out/lookup.js.map +1 -0
- package/out/schema-columns.d.ts +18 -19
- package/out/schema-columns.d.ts.map +1 -1
- package/out/schema-columns.js +14 -17
- package/out/schema-columns.js.map +1 -1
- package/out/sealed/build.d.ts +40 -0
- package/out/sealed/build.d.ts.map +1 -0
- package/out/{sealed-build.js → sealed/build.js} +3 -16
- package/out/sealed/build.js.map +1 -0
- package/out/sealed/db.d.ts +90 -0
- package/out/sealed/db.d.ts.map +1 -0
- package/out/{sealed-db.js → sealed/db.js} +63 -43
- package/out/sealed/db.js.map +1 -0
- package/out/sealed.d.ts +12 -8
- package/out/sealed.d.ts.map +1 -1
- package/out/sealed.js +11 -8
- package/out/sealed.js.map +1 -1
- package/out/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +15 -104
- package/lib/sealed-db.ts +0 -170
- package/out/dialect-config.d.ts.map +0 -1
- package/out/dialect-config.js.map +0 -1
- package/out/sealed-build.d.ts +0 -51
- package/out/sealed-build.d.ts.map +0 -1
- package/out/sealed-build.js.map +0 -1
- package/out/sealed-db.d.ts +0 -74
- package/out/sealed-db.d.ts.map +0 -1
- package/out/sealed-db.js.map +0 -1
- /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 `
|
|
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
|
|
16
|
-
* `true` on the call that committed
|
|
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 `
|
|
25
|
-
*
|
|
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
|
|
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
|
|
44
|
-
*
|
|
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.
|
|
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
|
|
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: `
|
|
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
|
|
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()`
|
|
147
|
-
*
|
|
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.
|
|
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"
|
package/lib/database-schema.ts
CHANGED
|
@@ -3,28 +3,10 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
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
|
|
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
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
|
|
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.
|
|
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
|
|
9
|
-
* file and
|
|
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
|
|
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
|
|
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
|
|
25
|
-
export * from "#sealed
|
|
25
|
+
export * from "#sealed/build"
|
|
26
|
+
export * from "#sealed/db"
|
package/lib/introspection.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
* @file Questions asked of `sqlite_master`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* reaches `node:sqlite`, and the docs demo bundles this package for the browser
|
|
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
|
|
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
|
-
* `
|
|
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
|
|
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.
|
|
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
|
|
51
|
-
*
|
|
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
|
+
}
|
package/lib/schema-columns.ts
CHANGED
|
@@ -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
|
|
14
|
-
*
|
|
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
|
|
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
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
38
|
-
* clustering into the B-tree penalizes
|
|
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
|
|
48
|
-
* {@link addBoundingBoxColumns} then {@link addRingsColumn}, with
|
|
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
|
|
58
|
-
*
|
|
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 `
|
|
62
|
-
* primary-key constraint and the raw `without rowid`
|
|
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
|
|
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.
|
|
15
|
+
* Where the sealed artifact lands.
|
|
24
16
|
*/
|
|
25
|
-
out:
|
|
17
|
+
out: PathBuilderLike
|
|
26
18
|
/**
|
|
27
|
-
* Create every table the build writes
|
|
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
|
|
33
|
-
*
|
|
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
|
|
38
|
-
*
|
|
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
|
|
43
|
-
* dropping any scratch table.
|
|
44
|
-
*
|
|
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
|
-
|
|
53
|
+
using kdb = new DatabaseClient<DB>(tmpPath)
|
|
65
54
|
|
|
66
55
|
try {
|
|
67
56
|
kdb.exec("PRAGMA journal_mode = OFF")
|