@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.
- package/lib/batched.ts +69 -0
- package/lib/client.ts +163 -0
- package/lib/database-schema.ts +30 -0
- package/lib/dialect-config.ts +30 -0
- package/lib/dialect.ts +67 -0
- package/lib/driver.ts +175 -0
- package/lib/index.ts +25 -0
- package/lib/introspection.ts +57 -0
- package/lib/schema-columns.ts +52 -0
- package/lib/sealed-build.ts +117 -0
- package/lib/sealed-db.ts +170 -0
- package/lib/sealed.ts +38 -0
- package/out/batched.d.ts +40 -0
- package/out/batched.d.ts.map +1 -0
- package/out/batched.js +41 -0
- package/out/batched.js.map +1 -0
- package/out/client.d.ts +86 -0
- package/out/client.d.ts.map +1 -0
- package/out/client.js +85 -0
- package/out/client.js.map +1 -0
- package/out/database-schema.d.ts +30 -0
- package/out/database-schema.d.ts.map +1 -0
- package/out/database-schema.js +22 -0
- package/out/database-schema.js.map +1 -0
- package/out/dialect-config.d.ts +27 -0
- package/out/dialect-config.d.ts.map +1 -0
- package/out/dialect-config.js +7 -0
- package/out/dialect-config.js.map +1 -0
- package/out/dialect.d.ts +39 -0
- package/out/dialect.d.ts.map +1 -0
- package/out/dialect.js +49 -0
- package/out/dialect.js.map +1 -0
- package/out/driver.d.ts +22 -0
- package/out/driver.d.ts.map +1 -0
- package/out/driver.js +114 -0
- package/out/driver.js.map +1 -0
- package/out/index.d.ts +25 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +25 -0
- package/out/index.js.map +1 -0
- package/out/introspection.d.ts +34 -0
- package/out/introspection.d.ts.map +1 -0
- package/out/introspection.js +46 -0
- package/out/introspection.js.map +1 -0
- package/out/schema-columns.d.ts +31 -0
- package/out/schema-columns.d.ts.map +1 -0
- package/out/schema-columns.js +38 -0
- package/out/schema-columns.js.map +1 -0
- package/out/sealed-build.d.ts +51 -0
- package/out/sealed-build.d.ts.map +1 -0
- package/out/sealed-build.js +71 -0
- package/out/sealed-build.js.map +1 -0
- package/out/sealed-db.d.ts +74 -0
- package/out/sealed-db.d.ts.map +1 -0
- package/out/sealed-db.js +140 -0
- package/out/sealed-db.js.map +1 -0
- package/out/sealed.d.ts +27 -0
- package/out/sealed.d.ts.map +1 -0
- package/out/sealed.js +31 -0
- package/out/sealed.js.map +1 -0
- package/out/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +172 -0
package/lib/batched.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* One open batched transaction over a connection.
|
|
12
|
+
*/
|
|
13
|
+
export interface BatchedTransaction {
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
rowWritten(): boolean
|
|
19
|
+
/**
|
|
20
|
+
* Commit the open transaction — the ordinary end of an ingest loop.
|
|
21
|
+
*/
|
|
22
|
+
commit(): void
|
|
23
|
+
/**
|
|
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.
|
|
27
|
+
*/
|
|
28
|
+
rollbackQuietly(): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
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.
|
|
36
|
+
*/
|
|
37
|
+
export function beginBatched(
|
|
38
|
+
database: { exec(sql: string): void },
|
|
39
|
+
options: { rowsPerCommit: number }
|
|
40
|
+
): BatchedTransaction {
|
|
41
|
+
let pending = 0
|
|
42
|
+
|
|
43
|
+
database.exec("BEGIN")
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
rowWritten() {
|
|
47
|
+
pending++
|
|
48
|
+
|
|
49
|
+
if (pending < options.rowsPerCommit) return false
|
|
50
|
+
|
|
51
|
+
database.exec("COMMIT")
|
|
52
|
+
database.exec("BEGIN")
|
|
53
|
+
|
|
54
|
+
pending = 0
|
|
55
|
+
|
|
56
|
+
return true
|
|
57
|
+
},
|
|
58
|
+
commit() {
|
|
59
|
+
database.exec("COMMIT")
|
|
60
|
+
},
|
|
61
|
+
rollbackQuietly() {
|
|
62
|
+
try {
|
|
63
|
+
database.exec("ROLLBACK")
|
|
64
|
+
} catch {
|
|
65
|
+
// The temp artifact is discarded either way.
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
package/lib/client.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DatabaseSync,
|
|
9
|
+
type DatabaseSyncOptions,
|
|
10
|
+
type FunctionOptions,
|
|
11
|
+
type SQLInputValue,
|
|
12
|
+
type SQLOutputValue,
|
|
13
|
+
type StatementSync,
|
|
14
|
+
} from "node:sqlite"
|
|
15
|
+
|
|
16
|
+
import { Kysely, type KyselyConfig } from "kysely"
|
|
17
|
+
import { PathBuilder, type PathBuilderLike } from "path-ts"
|
|
18
|
+
|
|
19
|
+
import type { Database } from "#database-schema"
|
|
20
|
+
import { SqliteDialect } from "#dialect"
|
|
21
|
+
import type { SqliteDialectConfig } from "#dialect-config"
|
|
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
|
+
/**
|
|
41
|
+
* A connection's non-Kysely surface: the statements Kysely does not model, plus ending the connection.
|
|
42
|
+
*
|
|
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.
|
|
47
|
+
*/
|
|
48
|
+
export type RawStatements = Pick<DatabaseClient, "exec" | "prepare" | "function" | "destroy"> & Disposable
|
|
49
|
+
|
|
50
|
+
export class DatabaseClient<DB = Database> extends Kysely<DB> implements Disposable {
|
|
51
|
+
/**
|
|
52
|
+
* A memory-only database, for tests and other ephemeral work. The connection is open until the client is disposed.
|
|
53
|
+
*/
|
|
54
|
+
public static temp<DB = Database>(): DatabaseClient<DB> {
|
|
55
|
+
return new DatabaseClient<DB>(":memory:")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
readonly #database: DatabaseSync
|
|
59
|
+
|
|
60
|
+
constructor(location: PathBuilderLike, options?: DatabaseSyncOptions, config?: Partial<KyselyConfig>)
|
|
61
|
+
constructor(database: DatabaseSync, config?: Partial<KyselyConfig>)
|
|
62
|
+
constructor(dialectConfig: SqliteDialectConfig, config?: Partial<KyselyConfig>)
|
|
63
|
+
constructor(
|
|
64
|
+
source: PathBuilderLike | DatabaseSync | SqliteDialectConfig,
|
|
65
|
+
optionsOrConfig?: DatabaseSyncOptions | Partial<KyselyConfig>,
|
|
66
|
+
config?: Partial<KyselyConfig>
|
|
67
|
+
) {
|
|
68
|
+
let database: DatabaseSync
|
|
69
|
+
let kyselyConfig: Partial<KyselyConfig> | undefined
|
|
70
|
+
|
|
71
|
+
if (typeof source === "string" || source instanceof PathBuilder) {
|
|
72
|
+
const location = source.toString()
|
|
73
|
+
const options = optionsOrConfig as DatabaseSyncOptions | undefined
|
|
74
|
+
// node:sqlite checks the argument count, not the value: `new DatabaseSync(path, undefined)` throws.
|
|
75
|
+
const openArgs: [string, DatabaseSyncOptions?] = options ? [location, options] : [location]
|
|
76
|
+
|
|
77
|
+
database = new DatabaseSync(...openArgs)
|
|
78
|
+
kyselyConfig = config
|
|
79
|
+
} else {
|
|
80
|
+
const dialectConfig = "database" in source ? source : { database: source }
|
|
81
|
+
|
|
82
|
+
if (typeof dialectConfig.database === "function") {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
"DatabaseClient needs an open connection: a lazy `() => Promise<DatabaseSync>` leaves `exec` and " +
|
|
85
|
+
"`prepare` with nothing to reach. Pass the database path instead."
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
database = dialectConfig.database
|
|
90
|
+
kyselyConfig = optionsOrConfig as Partial<KyselyConfig> | undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
super({
|
|
94
|
+
...kyselyConfig,
|
|
95
|
+
dialect: new SqliteDialect({ database }),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
this.#database = database
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
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.
|
|
106
|
+
*/
|
|
107
|
+
exec(sql: string): void {
|
|
108
|
+
this.#database.exec(sql)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 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
|
+
*/
|
|
118
|
+
prepare(sql: string): StatementSync {
|
|
119
|
+
return this.#database.prepare(sql)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register a user-defined SQL function on this client's connection, callable from any statement it runs.
|
|
124
|
+
*
|
|
125
|
+
* SQLite resolves the name at statement-compile time, so registration must precede the first query that uses it.
|
|
126
|
+
*/
|
|
127
|
+
function(name: string, options: FunctionOptions, fn: (...args: SQLOutputValue[]) => SQLInputValue): void
|
|
128
|
+
function(name: string, fn: (...args: SQLOutputValue[]) => SQLInputValue): void
|
|
129
|
+
function(
|
|
130
|
+
name: string,
|
|
131
|
+
optionsOrFn: FunctionOptions | ((...args: SQLOutputValue[]) => SQLInputValue),
|
|
132
|
+
fn?: (...args: SQLOutputValue[]) => SQLInputValue
|
|
133
|
+
): void {
|
|
134
|
+
if (typeof optionsOrFn === "function") {
|
|
135
|
+
this.#database.function(name, optionsOrFn)
|
|
136
|
+
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.#database.function(name, optionsOrFn, fn!)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* End the connection at scope exit, synchronously.
|
|
145
|
+
*
|
|
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.
|
|
153
|
+
*/
|
|
154
|
+
[Symbol.dispose](): void {
|
|
155
|
+
this.#database[Symbol.dispose]()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
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.
|
|
162
|
+
*/
|
|
163
|
+
export type { DatabaseSyncOptions, SQLInputValue, SQLOutputValue, StatementSync } from "node:sqlite"
|
|
@@ -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
|
+
/**
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
export type Database = Record<string, never>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DatabaseSync } from "node:sqlite"
|
|
8
|
+
|
|
9
|
+
import type { DatabaseConnection } from "kysely"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Config for the SQLite dialect.
|
|
13
|
+
*/
|
|
14
|
+
export interface SqliteDialectConfig {
|
|
15
|
+
/**
|
|
16
|
+
* A node:sqlite DatabaseSync instance or a function that returns one.
|
|
17
|
+
*
|
|
18
|
+
* If a function is provided, it's called once when the first query is executed.
|
|
19
|
+
*
|
|
20
|
+
* https://nodejs.org/api/sqlite.html#class-databasesync
|
|
21
|
+
*/
|
|
22
|
+
database: DatabaseSync | (() => Promise<DatabaseSync>)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Called once when the first query is executed.
|
|
26
|
+
*
|
|
27
|
+
* This is a Kysely specific feature and does not come from the `node:sqlite` module.
|
|
28
|
+
*/
|
|
29
|
+
onCreateConnection?: (connection: DatabaseConnection) => Promise<void>
|
|
30
|
+
}
|
package/lib/dialect.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
SqliteAdapter,
|
|
9
|
+
SqliteIntrospector,
|
|
10
|
+
SqliteQueryCompiler,
|
|
11
|
+
type DatabaseIntrospector,
|
|
12
|
+
type Dialect,
|
|
13
|
+
type DialectAdapter,
|
|
14
|
+
type Driver,
|
|
15
|
+
type Kysely,
|
|
16
|
+
type QueryCompiler,
|
|
17
|
+
} from "kysely"
|
|
18
|
+
|
|
19
|
+
import type { SqliteDialectConfig } from "#dialect-config"
|
|
20
|
+
import { SqliteDriver } from "#driver"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SQLite dialect that uses the [`node:sqlite`](https://nodejs.org/api/sqlite.html) library.
|
|
24
|
+
*
|
|
25
|
+
* The constructor takes an instance of {@link SqliteDialectConfig}.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { DatabaseSync } from "node:sqlite"
|
|
29
|
+
*
|
|
30
|
+
* new SqliteDialect({
|
|
31
|
+
* database: new DatabaseSync("db.sqlite"),
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* If you want the pool to only be created once it's first used, `database` can be a function:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { DatabaseSync } from "node:sqlite"
|
|
39
|
+
*
|
|
40
|
+
* new SqliteDialect({
|
|
41
|
+
* database: async () => new DatabaseSync("db.sqlite"),
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class SqliteDialect implements Dialect {
|
|
46
|
+
protected config: SqliteDialectConfig
|
|
47
|
+
|
|
48
|
+
constructor(config: SqliteDialectConfig) {
|
|
49
|
+
this.config = Object.freeze({ ...config })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
createDriver(): Driver {
|
|
53
|
+
return new SqliteDriver(this.config)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createQueryCompiler(): QueryCompiler {
|
|
57
|
+
return new SqliteQueryCompiler()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
createAdapter(): DialectAdapter {
|
|
61
|
+
return new SqliteAdapter()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
createIntrospector(db: Kysely<unknown>): DatabaseIntrospector {
|
|
65
|
+
return new SqliteIntrospector(db)
|
|
66
|
+
}
|
|
67
|
+
}
|
package/lib/driver.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DatabaseSync, StatementSync } from "node:sqlite"
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CompiledQuery,
|
|
11
|
+
createQueryId,
|
|
12
|
+
type DatabaseConnection,
|
|
13
|
+
type Driver,
|
|
14
|
+
IdentifierNode,
|
|
15
|
+
type QueryCompiler,
|
|
16
|
+
type QueryResult,
|
|
17
|
+
RawNode,
|
|
18
|
+
SelectQueryNode,
|
|
19
|
+
} from "kysely"
|
|
20
|
+
|
|
21
|
+
import type { SqliteDialectConfig } from "#dialect-config"
|
|
22
|
+
|
|
23
|
+
class ConnectionMutex {
|
|
24
|
+
#promise?: Promise<void>
|
|
25
|
+
#resolve?: () => void
|
|
26
|
+
|
|
27
|
+
async lock(): Promise<void> {
|
|
28
|
+
while (this.#promise) {
|
|
29
|
+
await this.#promise
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.#promise = new Promise((resolve) => {
|
|
33
|
+
this.#resolve = resolve
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
unlock(): void {
|
|
38
|
+
const resolve = this.#resolve
|
|
39
|
+
|
|
40
|
+
this.#promise = undefined
|
|
41
|
+
this.#resolve = undefined
|
|
42
|
+
|
|
43
|
+
resolve?.()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class SqliteDriver implements Driver {
|
|
48
|
+
readonly #config: SqliteDialectConfig
|
|
49
|
+
readonly #connectionMutex = new ConnectionMutex()
|
|
50
|
+
|
|
51
|
+
#db?: DatabaseSync
|
|
52
|
+
#connection?: DatabaseConnection
|
|
53
|
+
|
|
54
|
+
constructor(config: SqliteDialectConfig) {
|
|
55
|
+
this.#config = Object.freeze({ ...config })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async init(): Promise<void> {
|
|
59
|
+
const db = typeof this.#config.database === "function" ? await this.#config.database() : this.#config.database
|
|
60
|
+
|
|
61
|
+
this.#db = db
|
|
62
|
+
this.#connection = new SqliteConnection(db)
|
|
63
|
+
|
|
64
|
+
if (this.#config.onCreateConnection) {
|
|
65
|
+
await this.#config.onCreateConnection(this.#connection)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
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.
|
|
72
|
+
await this.#connectionMutex.lock()
|
|
73
|
+
|
|
74
|
+
// biome-ignore lint/style/noNonNullAssertion: :shrug:
|
|
75
|
+
return this.#connection!
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
|
79
|
+
await connection.executeQuery(CompiledQuery.raw("begin"))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
|
83
|
+
await connection.executeQuery(CompiledQuery.raw("commit"))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
|
87
|
+
await connection.executeQuery(CompiledQuery.raw("rollback"))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async savepoint(
|
|
91
|
+
connection: DatabaseConnection,
|
|
92
|
+
savepointName: string,
|
|
93
|
+
compileQuery: QueryCompiler["compileQuery"]
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
await connection.executeQuery(compileQuery(parseSavepointCommand("savepoint", savepointName), createQueryId()))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async rollbackToSavepoint(
|
|
99
|
+
connection: DatabaseConnection,
|
|
100
|
+
savepointName: string,
|
|
101
|
+
compileQuery: QueryCompiler["compileQuery"]
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
await connection.executeQuery(compileQuery(parseSavepointCommand("rollback to", savepointName), createQueryId()))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async releaseSavepoint(
|
|
107
|
+
connection: DatabaseConnection,
|
|
108
|
+
savepointName: string,
|
|
109
|
+
compileQuery: QueryCompiler["compileQuery"]
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
await connection.executeQuery(compileQuery(parseSavepointCommand("release", savepointName), createQueryId()))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async releaseConnection(): Promise<void> {
|
|
115
|
+
this.#connectionMutex.unlock()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async destroy(): Promise<void> {
|
|
119
|
+
this.#db?.[Symbol.dispose]()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class SqliteConnection implements DatabaseConnection {
|
|
124
|
+
readonly #db: DatabaseSync
|
|
125
|
+
|
|
126
|
+
constructor(db: DatabaseSync) {
|
|
127
|
+
this.#db = db
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
|
|
131
|
+
const { sql, parameters } = compiledQuery
|
|
132
|
+
const stmt = this.#db.prepare(sql)
|
|
133
|
+
|
|
134
|
+
const args = Array.isArray(parameters) ? parameters : []
|
|
135
|
+
|
|
136
|
+
if (stmt.columns().length) {
|
|
137
|
+
const rows = stmt.all(...args) as O[]
|
|
138
|
+
|
|
139
|
+
return Promise.resolve({ rows })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = stmt.run(...args)
|
|
143
|
+
|
|
144
|
+
return Promise.resolve({
|
|
145
|
+
numAffectedRows: BigInt(result.changes),
|
|
146
|
+
insertID: BigInt(result.lastInsertRowid),
|
|
147
|
+
rows: [],
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async *streamQuery<R>(compiledQuery: CompiledQuery, _chunkSize: number): AsyncIterableIterator<QueryResult<R>> {
|
|
152
|
+
const { sql, parameters, query } = compiledQuery
|
|
153
|
+
const stmt: StatementSync = this.#db.prepare(sql)
|
|
154
|
+
const args = Array.isArray(parameters) ? parameters : []
|
|
155
|
+
|
|
156
|
+
if (!SelectQueryNode.is(query)) {
|
|
157
|
+
throw new Error("Sqlite driver only supports streaming of select queries")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const iter = stmt.iterate(...args) as IterableIterator<R>
|
|
161
|
+
|
|
162
|
+
for (const row of iter) {
|
|
163
|
+
yield {
|
|
164
|
+
rows: [row],
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseSavepointCommand(command: string, savepointName: string): RawNode {
|
|
171
|
+
return RawNode.createWithChildren([
|
|
172
|
+
RawNode.createWithSql(`${command} `),
|
|
173
|
+
IdentifierNode.create(savepointName), // ensures savepointName gets sanitized
|
|
174
|
+
])
|
|
175
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The one way a Mailwoman SQLite connection comes into being.
|
|
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
|
|
10
|
+
* lifetime `using` can end. `new DatabaseSync` appears once in this package and nowhere else in the repository.
|
|
11
|
+
*
|
|
12
|
+
* It lives outside `@mailwoman/core` because it needs none of core's 11 MB of parser reference data — its whole
|
|
13
|
+
* dependency set is `kysely`. That is what lets the leaf lookups use it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export * from "#batched"
|
|
17
|
+
export * from "#client"
|
|
18
|
+
export * from "#database-schema"
|
|
19
|
+
export * from "#dialect"
|
|
20
|
+
export * from "#dialect-config"
|
|
21
|
+
export * from "#introspection"
|
|
22
|
+
export * from "#schema-columns"
|
|
23
|
+
export * from "#sealed"
|
|
24
|
+
export * from "#sealed-build"
|
|
25
|
+
export * from "#sealed-db"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file Questions asked of `sqlite_master`.
|
|
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
|
|
9
|
+
* ask whether a table exists dragged the builtin into that graph. The split is what keeps these three
|
|
10
|
+
* pure-SQL questions reachable from a bundled consumer.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DatabaseClient } from "#client"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether a TABLE named `name` exists. An index, view or trigger of that name answers `false`.
|
|
17
|
+
*/
|
|
18
|
+
export function tableExists<DB>(db: DatabaseClient<DB>, name: string): boolean {
|
|
19
|
+
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `COUNT(*)` over `table`. The name is interpolated, so it must come from the schema, never from input.
|
|
24
|
+
*/
|
|
25
|
+
export function countRows<DB>(db: DatabaseClient<DB>, table: string): number {
|
|
26
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n?: number } | undefined
|
|
27
|
+
|
|
28
|
+
return Number(row?.n ?? 0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether an INDEX named `name` exists.
|
|
33
|
+
*/
|
|
34
|
+
export function indexExists<DB>(db: DatabaseClient<DB>, name: string): boolean {
|
|
35
|
+
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?").get(name))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every table name, sorted. Virtual tables are tables here, the way `sqlite_master` records them.
|
|
40
|
+
*/
|
|
41
|
+
export function listTables<DB>(db: DatabaseClient<DB>): string[] {
|
|
42
|
+
const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all() as Array<{
|
|
43
|
+
name: string
|
|
44
|
+
}>
|
|
45
|
+
|
|
46
|
+
return rows.map((row) => row.name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
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.
|
|
52
|
+
*/
|
|
53
|
+
export function hasColumn<DB>(db: DatabaseClient<DB>, table: string, column: string): boolean {
|
|
54
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>
|
|
55
|
+
|
|
56
|
+
return rows.some((row) => row.name === column)
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CreateTableBuilder, Kysely } from "kysely"
|
|
11
|
+
|
|
12
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
export type SchemaHandle<DB> = Pick<Kysely<DB>, "schema">
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The precomputed bounding box plus the unsimplified ring blob every polygon truth table carries.
|
|
20
|
+
*
|
|
21
|
+
* A table taking these stays a PLAIN rowid table, never `WITHOUT ROWID`: the `rings` blob is exactly the payload
|
|
22
|
+
* clustering into the B-tree penalizes — every index page becomes a geometry page.
|
|
23
|
+
*/
|
|
24
|
+
export function addRingGeometryColumns<TB extends string, C extends string>(
|
|
25
|
+
builder: CreateTableBuilder<TB, C>
|
|
26
|
+
): CreateTableBuilder<TB, C | "min_lat" | "min_lon" | "max_lat" | "max_lon" | "rings"> {
|
|
27
|
+
return builder
|
|
28
|
+
.addColumn("min_lat", "real", (column) => column.notNull())
|
|
29
|
+
.addColumn("min_lon", "real", (column) => column.notNull())
|
|
30
|
+
.addColumn("max_lat", "real", (column) => column.notNull())
|
|
31
|
+
.addColumn("max_lon", "real", (column) => column.notNull())
|
|
32
|
+
.addColumn("rings", "blob", (column) => column.notNull())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The cell-index columns every polygon summary tier carries: the 48-bit short cell, the resolution it was captured at
|
|
37
|
+
* (a short cell does not name its own, and a mixed-resolution table cannot be probed without it), the key the row
|
|
38
|
+
* names, and its containment.
|
|
39
|
+
*
|
|
40
|
+
* Small fixed-width rows probed by their exact primary key are the `WITHOUT ROWID` shape — the caller adds its own
|
|
41
|
+
* primary-key constraint and the raw `without rowid` modifier, because the key differs per layer.
|
|
42
|
+
*/
|
|
43
|
+
export function addCellIndexColumns<TB extends string, C extends string, K extends string>(
|
|
44
|
+
builder: CreateTableBuilder<TB, C>,
|
|
45
|
+
keyColumn: K
|
|
46
|
+
): CreateTableBuilder<TB, C | K | "h3_cell" | "resolution" | "containment"> {
|
|
47
|
+
return builder
|
|
48
|
+
.addColumn("h3_cell", "integer", (column) => column.notNull())
|
|
49
|
+
.addColumn("resolution", "integer", (column) => column.notNull())
|
|
50
|
+
.addColumn(keyColumn, "text", (column) => column.notNull())
|
|
51
|
+
.addColumn("containment", "text", (column) => column.notNull())
|
|
52
|
+
}
|