@mailwoman/sqlite 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/batched.ts +69 -0
  2. package/lib/client.ts +163 -0
  3. package/lib/database-schema.ts +30 -0
  4. package/lib/dialect-config.ts +30 -0
  5. package/lib/dialect.ts +67 -0
  6. package/lib/driver.ts +175 -0
  7. package/lib/index.ts +25 -0
  8. package/lib/introspection.ts +57 -0
  9. package/lib/schema-columns.ts +52 -0
  10. package/lib/sealed-build.ts +117 -0
  11. package/lib/sealed-db.ts +170 -0
  12. package/lib/sealed.ts +38 -0
  13. package/out/batched.d.ts +40 -0
  14. package/out/batched.d.ts.map +1 -0
  15. package/out/batched.js +41 -0
  16. package/out/batched.js.map +1 -0
  17. package/out/client.d.ts +86 -0
  18. package/out/client.d.ts.map +1 -0
  19. package/out/client.js +85 -0
  20. package/out/client.js.map +1 -0
  21. package/out/database-schema.d.ts +30 -0
  22. package/out/database-schema.d.ts.map +1 -0
  23. package/out/database-schema.js +22 -0
  24. package/out/database-schema.js.map +1 -0
  25. package/out/dialect-config.d.ts +27 -0
  26. package/out/dialect-config.d.ts.map +1 -0
  27. package/out/dialect-config.js +7 -0
  28. package/out/dialect-config.js.map +1 -0
  29. package/out/dialect.d.ts +39 -0
  30. package/out/dialect.d.ts.map +1 -0
  31. package/out/dialect.js +49 -0
  32. package/out/dialect.js.map +1 -0
  33. package/out/driver.d.ts +22 -0
  34. package/out/driver.d.ts.map +1 -0
  35. package/out/driver.js +114 -0
  36. package/out/driver.js.map +1 -0
  37. package/out/index.d.ts +25 -0
  38. package/out/index.d.ts.map +1 -0
  39. package/out/index.js +25 -0
  40. package/out/index.js.map +1 -0
  41. package/out/introspection.d.ts +34 -0
  42. package/out/introspection.d.ts.map +1 -0
  43. package/out/introspection.js +46 -0
  44. package/out/introspection.js.map +1 -0
  45. package/out/schema-columns.d.ts +31 -0
  46. package/out/schema-columns.d.ts.map +1 -0
  47. package/out/schema-columns.js +38 -0
  48. package/out/schema-columns.js.map +1 -0
  49. package/out/sealed-build.d.ts +51 -0
  50. package/out/sealed-build.d.ts.map +1 -0
  51. package/out/sealed-build.js +71 -0
  52. package/out/sealed-build.js.map +1 -0
  53. package/out/sealed-db.d.ts +74 -0
  54. package/out/sealed-db.d.ts.map +1 -0
  55. package/out/sealed-db.js +140 -0
  56. package/out/sealed-db.js.map +1 -0
  57. package/out/sealed.d.ts +27 -0
  58. package/out/sealed.d.ts.map +1 -0
  59. package/out/sealed.js +31 -0
  60. package/out/sealed.js.map +1 -0
  61. package/out/tsconfig.test.tsbuildinfo +1 -0
  62. package/package.json +172 -0
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
+ }