@jarenjs/db 0.34.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 (83) hide show
  1. package/ARCHITECTURE.md +397 -0
  2. package/README.md +218 -0
  3. package/dist/types/algebra.d.ts +133 -0
  4. package/dist/types/app.d.ts +49 -0
  5. package/dist/types/capture.d.ts +85 -0
  6. package/dist/types/cli.d.ts +2 -0
  7. package/dist/types/dag-job.d.ts +40 -0
  8. package/dist/types/ddl.d.ts +170 -0
  9. package/dist/types/dialect.d.ts +130 -0
  10. package/dist/types/dialects/sqlite.d.ts +9 -0
  11. package/dist/types/driver.d.ts +128 -0
  12. package/dist/types/drivers/bun.d.ts +47 -0
  13. package/dist/types/drivers/node.d.ts +37 -0
  14. package/dist/types/drivers/wasm.d.ts +65 -0
  15. package/dist/types/emit-model.d.ts +44 -0
  16. package/dist/types/emit.d.ts +72 -0
  17. package/dist/types/entity.d.ts +23 -0
  18. package/dist/types/errors.d.ts +165 -0
  19. package/dist/types/graph.d.ts +28 -0
  20. package/dist/types/index.d.ts +35 -0
  21. package/dist/types/jobs.d.ts +134 -0
  22. package/dist/types/live.d.ts +62 -0
  23. package/dist/types/migrate.d.ts +163 -0
  24. package/dist/types/model.d.ts +36 -0
  25. package/dist/types/patch-sql.d.ts +37 -0
  26. package/dist/types/plan.d.ts +119 -0
  27. package/dist/types/profile.d.ts +80 -0
  28. package/dist/types/query.d.ts +100 -0
  29. package/dist/types/residual.d.ts +50 -0
  30. package/dist/types/store.d.ts +53 -0
  31. package/dist/types/tracker.d.ts +43 -0
  32. package/dist/types/typed.d.ts +15 -0
  33. package/dist/types/types.d.ts +26 -0
  34. package/dist/types/udf.d.ts +70 -0
  35. package/dist/types/window.d.ts +52 -0
  36. package/docs/JOBS-FORMAT.md +218 -0
  37. package/docs/LIVE-FORMAT.md +348 -0
  38. package/docs/MIGRATION-FORMAT.md +302 -0
  39. package/docs/MODEL-FORMAT.md +928 -0
  40. package/package.json +81 -0
  41. package/schemas/jaren-migration.draft-07.schema.json +144 -0
  42. package/schemas/jaren-migration.schema.json +144 -0
  43. package/schemas/jaren-model.draft-07.schema.json +149 -0
  44. package/schemas/jaren-model.schema.json +149 -0
  45. package/src/algebra.js +105 -0
  46. package/src/app.js +108 -0
  47. package/src/capture.js +584 -0
  48. package/src/cli.js +264 -0
  49. package/src/dag-job.js +86 -0
  50. package/src/ddl.js +588 -0
  51. package/src/dialect.js +297 -0
  52. package/src/dialects/sqlite.js +175 -0
  53. package/src/driver.js +419 -0
  54. package/src/drivers/bun.js +101 -0
  55. package/src/drivers/node.js +93 -0
  56. package/src/drivers/wasm.js +178 -0
  57. package/src/emit-model.js +208 -0
  58. package/src/emit.js +393 -0
  59. package/src/entity.js +367 -0
  60. package/src/errors.js +173 -0
  61. package/src/graph.js +101 -0
  62. package/src/index.js +64 -0
  63. package/src/jobs.js +507 -0
  64. package/src/live.js +899 -0
  65. package/src/migrate.js +1411 -0
  66. package/src/model.js +476 -0
  67. package/src/patch-sql.js +150 -0
  68. package/src/plan.js +1038 -0
  69. package/src/profile.js +131 -0
  70. package/src/query.js +1010 -0
  71. package/src/residual.js +91 -0
  72. package/src/store.js +1422 -0
  73. package/src/tracker.js +776 -0
  74. package/src/typed.js +19 -0
  75. package/src/types.js +36 -0
  76. package/src/udf.js +132 -0
  77. package/src/window.js +125 -0
  78. package/types/app.d.ts +36 -0
  79. package/types/bun.d.ts +9 -0
  80. package/types/index.d.ts +592 -0
  81. package/types/node.d.ts +15 -0
  82. package/types/typed.d.ts +108 -0
  83. package/types/wasm.d.ts +5 -0
package/src/dialect.js ADDED
@@ -0,0 +1,297 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The dialect seam: the ONLY place SQL text is produced. A
4
+ * dialect is data plus a small emitter — a spelling spec (how to quote
5
+ * an identifier, reference a parameter, extract a JSON member, open a
6
+ * savepoint) composed by {@link createDialect} into the DDL and DML
7
+ * statement builders the store consumes. Nothing outside a dialect
8
+ * concatenates SQL; that costs one indirection now, and without it a
9
+ * second backend is a rewrite.
10
+ *
11
+ * Deliberately NOT in the dialect, because they are behavioural rather
12
+ * than syntactic: whether functions can be registered per connection,
13
+ * whether change capture exists and in what form, and whether tables
14
+ * can be restructured in place. Those are capabilities on the
15
+ * connection.
16
+ */
17
+
18
+ /**
19
+ * A typed member path into the JSON document column: name segments for
20
+ * object members, index segments for array positions. Produced by the
21
+ * DDL planner (from analyzed index paths) and the patch translator
22
+ * (from pointers discriminated against the live document).
23
+ * @typedef {{ name: string } | { index: number }} JsonPathSegment
24
+ */
25
+
26
+ /**
27
+ * Compose a dialect from its spelling spec. Every statement the store
28
+ * ever runs is built here from the spec's primitives, so a spec with
29
+ * different quoting or parameter style produces correspondingly
30
+ * different SQL from the same model — the property the test-double
31
+ * dialect pins.
32
+ * @param {{
33
+ * name: string,
34
+ * capabilities: Record<string, any>,
35
+ * tableSuffix: string,
36
+ * docColumnType: string,
37
+ * quoteIdentifier: (s: string) => string,
38
+ * parameterRef: (i: number, name: string) => string,
39
+ * stringLiteral: (s: string) => string,
40
+ * booleanLiteral: (b: boolean) => string,
41
+ * typeFor: (schemaType: string | undefined, hint: string) => string,
42
+ * limitClause: (limit: number, offset?: number) => string,
43
+ * jsonPathText: (segments: JsonPathSegment[]) => string | null,
44
+ * jsonExtract: (columnSql: string, pathText: string) => string,
45
+ * jsonSet: (exprSql: string, pathText: string, valueSql: string) => string,
46
+ * jsonRemove: (exprSql: string, pathText: string) => string,
47
+ * jsonAppend: (exprSql: string, arrayPathText: string, valueSql: string) => string,
48
+ * jsonEncode: (paramSql: string) => string,
49
+ * jsonText: (columnSql: string) => string,
50
+ * jsonAgg: (exprSql: string) => string,
51
+ * jsonTypeOf: (columnSql: string, pathText: string) => string,
52
+ * valueTypeOf: (paramSql: string) => string,
53
+ * strStartsWith: (valueSql: string, patternA: string, patternB: string) => string,
54
+ * strEndsWith: (valueSql: string, patternA: string, patternB: string, patternC: string) => string,
55
+ * strContains: (valueSql: string, patternSql: string) => string,
56
+ * orderNulls: (nullsFirst: boolean) => string,
57
+ * rowIdentity: () => string,
58
+ * explainQuery: (sql: string) => string,
59
+ * excludedRef: (columnSql: string) => string,
60
+ * tx: { begin: string, beginImmediate: string, commit: string,
61
+ * rollback: string,
62
+ * savepoint: (n: string) => string, release: (n: string) => string,
63
+ * rollbackTo: (n: string) => string },
64
+ * pragma: { busyTimeout: (ms: number) => string,
65
+ * journalMode: (mode: string) => string,
66
+ * foreignKeys: (on: boolean) => string },
67
+ * introspect: { version: () => string, compileOptions: () => string,
68
+ * tableExists: () => string, columns: (table: string) => string,
69
+ * indexes: (table: string) => string,
70
+ * indexColumns: (index: string) => string,
71
+ * foreignKeysOn: () => string,
72
+ * foreignKeyList: (table: string) => string },
73
+ * }} spec
74
+ * @returns {any} the frozen dialect
75
+ */
76
+ export function createDialect(spec) {
77
+ const q = spec.quoteIdentifier;
78
+ const p = spec.parameterRef;
79
+
80
+ const ddl = Object.freeze({
81
+ /**
82
+ * One collection's physical table: a key column, the JSON document
83
+ * column, and a virtual generated column per indexed path.
84
+ * @param {{ table: string, keyColumn: string, keyType: string,
85
+ * docColumn: string, generated: { name: string, type: string,
86
+ * pathText: string }[] }} shape
87
+ * @returns {string}
88
+ */
89
+ createTable({ table, keyColumn, keyType, docColumn, generated }) {
90
+ const columns = [
91
+ `${q(keyColumn)} ${keyType} PRIMARY KEY`,
92
+ `${q(docColumn)} ${spec.docColumnType} NOT NULL`,
93
+ ...generated.map((g) =>
94
+ `${q(g.name)} ${g.type} GENERATED ALWAYS AS `
95
+ + `(${spec.jsonExtract(q(docColumn), g.pathText)}) VIRTUAL`),
96
+ ];
97
+ return `CREATE TABLE ${q(table)} (${columns.join(', ')})${spec.tableSuffix}`;
98
+ },
99
+ /**
100
+ * @param {{ name: string, table: string, columns: string[],
101
+ * unique: boolean }} shape
102
+ * @returns {string}
103
+ */
104
+ createIndex({ name, table, columns, unique }) {
105
+ return `CREATE ${unique ? 'UNIQUE ' : ''}INDEX ${q(name)} `
106
+ + `ON ${q(table)} (${columns.map(q).join(', ')})`;
107
+ },
108
+ /**
109
+ * Add one virtual generated column to an existing table (a new
110
+ * indexed path arriving through a migration).
111
+ * @param {{ table: string, docColumn: string,
112
+ * column: { name: string, type: string, pathText: string } }} shape
113
+ * @returns {string}
114
+ */
115
+ addGeneratedColumn({ table, docColumn, column }) {
116
+ return `ALTER TABLE ${q(table)} ADD COLUMN ${q(column.name)} ${column.type} `
117
+ + `GENERATED ALWAYS AS (${spec.jsonExtract(q(docColumn), column.pathText)}) VIRTUAL`;
118
+ },
119
+ /**
120
+ * @param {string} table
121
+ * @param {string} column
122
+ * @returns {string}
123
+ */
124
+ dropColumn(table, column) {
125
+ return `ALTER TABLE ${q(table)} DROP COLUMN ${q(column)}`;
126
+ },
127
+ /**
128
+ * Add one plain (non-generated) column — the additive migration
129
+ * strategy. The column shape matches `createRelationalTable`'s.
130
+ * @param {{ table: string, column: { name: string, type: string,
131
+ * check?: string, references?: { table: string, column: string,
132
+ * onDelete: 'cascade' | 'restrict' | 'setNull' } } }} shape
133
+ * @returns {string}
134
+ */
135
+ addColumn({ table, column }) {
136
+ const onDeleteSql = { cascade: 'CASCADE', restrict: 'RESTRICT', setNull: 'SET NULL' };
137
+ let sql = `ALTER TABLE ${q(table)} ADD COLUMN ${q(column.name)} ${column.type}`;
138
+ if (column.check !== undefined) sql += ` CHECK (${column.check})`;
139
+ if (column.references !== undefined) {
140
+ sql += ` REFERENCES ${q(column.references.table)} (${q(column.references.column)})`
141
+ + ` ON DELETE ${onDeleteSql[column.references.onDelete]}`;
142
+ }
143
+ return sql;
144
+ },
145
+ /**
146
+ * @param {string} name
147
+ * @returns {string}
148
+ */
149
+ dropIndex(name) {
150
+ return `DROP INDEX ${q(name)}`;
151
+ },
152
+ /**
153
+ * @param {string} table
154
+ * @returns {string}
155
+ */
156
+ dropTable(table) {
157
+ return `DROP TABLE ${q(table)}`;
158
+ },
159
+ /**
160
+ * @param {string} from
161
+ * @param {string} to
162
+ * @returns {string}
163
+ */
164
+ renameTable(from, to) {
165
+ return `ALTER TABLE ${q(from)} RENAME TO ${q(to)}`;
166
+ },
167
+ /**
168
+ * @param {string} table
169
+ * @param {string} from
170
+ * @param {string} to
171
+ * @returns {string}
172
+ */
173
+ renameColumn(table, from, to) {
174
+ return `ALTER TABLE ${q(table)} RENAME COLUMN ${q(from)} TO ${q(to)}`;
175
+ },
176
+ /**
177
+ * A relational entity table: typed columns (keys, mapped scalars,
178
+ * foreign keys), per-column CHECKs, real REFERENCES clauses with
179
+ * declared on-delete behaviour, and the JSONB document column for
180
+ * everything unmapped. One generator, both document kinds.
181
+ * @param {{ table: string, columns: {
182
+ * name: string, type: string, notNull?: boolean,
183
+ * primaryKey?: boolean, check?: string,
184
+ * references?: { table: string, column: string,
185
+ * onDelete: 'cascade' | 'restrict' | 'setNull' } }[],
186
+ * compositeKey?: string[] }} shape
187
+ * @returns {string}
188
+ */
189
+ createRelationalTable({ table, columns, compositeKey }) {
190
+ const onDeleteSql = { cascade: 'CASCADE', restrict: 'RESTRICT', setNull: 'SET NULL' };
191
+ const rendered = columns.map((column) => {
192
+ let sql = `${q(column.name)} ${column.type}`;
193
+ if (column.primaryKey === true) sql += ' PRIMARY KEY';
194
+ if (column.notNull === true) sql += ' NOT NULL';
195
+ if (column.check !== undefined) sql += ` CHECK (${column.check})`;
196
+ if (column.references !== undefined) {
197
+ sql += ` REFERENCES ${q(column.references.table)} (${q(column.references.column)})`
198
+ + ` ON DELETE ${onDeleteSql[column.references.onDelete]}`;
199
+ }
200
+ return sql;
201
+ });
202
+ if (compositeKey !== undefined && compositeKey.length > 0)
203
+ rendered.push(`PRIMARY KEY (${compositeKey.map(q).join(', ')})`);
204
+ return `CREATE TABLE ${q(table)} (${rendered.join(', ')})${spec.tableSuffix}`;
205
+ },
206
+ /**
207
+ * A plain (non-collection) table — the migration history table.
208
+ * @param {{ table: string, columns: { name: string, type: string,
209
+ * primaryKey?: boolean }[] }} shape
210
+ * @returns {string}
211
+ */
212
+ createPlainTable({ table, columns }) {
213
+ const rendered = columns.map((column) =>
214
+ `${q(column.name)} ${column.type}${column.primaryKey === true ? ' PRIMARY KEY' : ''}`);
215
+ return `CREATE TABLE IF NOT EXISTS ${q(table)} (${rendered.join(', ')})${spec.tableSuffix}`;
216
+ },
217
+ });
218
+
219
+ const dml = Object.freeze({
220
+ /** @param {{ table: string, keyColumn: string, docColumn: string }} s */
221
+ insert({ table, keyColumn, docColumn }) {
222
+ return `INSERT INTO ${q(table)} (${q(keyColumn)}, ${q(docColumn)}) `
223
+ + `VALUES (${p(1, 'key')}, ${spec.jsonEncode(p(2, 'doc'))})`;
224
+ },
225
+ /**
226
+ * Insert with a database-allocated key, read back in the same
227
+ * statement.
228
+ * @param {{ table: string, keyColumn: string, docColumn: string }} s
229
+ */
230
+ insertAllocated({ table, keyColumn, docColumn }) {
231
+ return `INSERT INTO ${q(table)} (${q(docColumn)}) `
232
+ + `VALUES (${spec.jsonEncode(p(1, 'doc'))}) RETURNING ${q(keyColumn)} AS ${q('key')}`;
233
+ },
234
+ /** @param {{ table: string, keyColumn: string, docColumn: string }} s */
235
+ upsert({ table, keyColumn, docColumn }) {
236
+ return `INSERT INTO ${q(table)} (${q(keyColumn)}, ${q(docColumn)}) `
237
+ + `VALUES (${p(1, 'key')}, ${spec.jsonEncode(p(2, 'doc'))}) `
238
+ + `ON CONFLICT (${q(keyColumn)}) DO UPDATE SET `
239
+ + `${q(docColumn)} = ${spec.excludedRef(q(docColumn))}`;
240
+ },
241
+ /** @param {{ table: string, keyColumn: string, docColumn: string }} s */
242
+ get({ table, keyColumn, docColumn }) {
243
+ return `SELECT ${spec.jsonText(q(docColumn))} AS ${q('doc')} `
244
+ + `FROM ${q(table)} WHERE ${q(keyColumn)} = ${p(1, 'key')}`;
245
+ },
246
+ /** @param {{ table: string, keyColumn: string }} s */
247
+ del({ table, keyColumn }) {
248
+ return `DELETE FROM ${q(table)} WHERE ${q(keyColumn)} = ${p(1, 'key')}`;
249
+ },
250
+ /**
251
+ * Rewrite the document column through a JSON-set expression chain
252
+ * (the translated-patch path) or a bound parameter (the fallback).
253
+ * @param {{ table: string, keyColumn: string, docColumn: string }} s
254
+ * @param {string} expression - SQL over the document column
255
+ * @param {number} keyIndex - 1-based position of the key parameter
256
+ */
257
+ updateDoc({ table, keyColumn, docColumn }, expression, keyIndex) {
258
+ return `UPDATE ${q(table)} SET ${q(docColumn)} = ${expression} `
259
+ + `WHERE ${q(keyColumn)} = ${p(keyIndex, 'key')}`;
260
+ },
261
+ });
262
+
263
+ return Object.freeze({
264
+ name: spec.name,
265
+ capabilities: Object.freeze({ ...spec.capabilities }),
266
+ docColumnType: spec.docColumnType,
267
+ quoteIdentifier: q,
268
+ parameterRef: p,
269
+ stringLiteral: spec.stringLiteral,
270
+ booleanLiteral: spec.booleanLiteral,
271
+ typeFor: spec.typeFor,
272
+ limitClause: spec.limitClause,
273
+ jsonPathText: spec.jsonPathText,
274
+ jsonExtract: spec.jsonExtract,
275
+ jsonSet: spec.jsonSet,
276
+ jsonRemove: spec.jsonRemove,
277
+ jsonAppend: spec.jsonAppend,
278
+ jsonEncode: spec.jsonEncode,
279
+ jsonText: spec.jsonText,
280
+ jsonAgg: spec.jsonAgg,
281
+ jsonTypeOf: spec.jsonTypeOf,
282
+ valueTypeOf: spec.valueTypeOf,
283
+ strStartsWith: spec.strStartsWith,
284
+ strEndsWith: spec.strEndsWith,
285
+ strContains: spec.strContains,
286
+ orderNulls: spec.orderNulls,
287
+ rowIdentity: spec.rowIdentity,
288
+ explainQuery: spec.explainQuery,
289
+ excludedRef: spec.excludedRef,
290
+ epochFromRfc3339: spec.epochFromRfc3339,
291
+ tx: Object.freeze({ ...spec.tx }),
292
+ pragma: Object.freeze({ ...spec.pragma }),
293
+ introspect: Object.freeze({ ...spec.introspect }),
294
+ ddl,
295
+ dml,
296
+ });
297
+ }
@@ -0,0 +1,175 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The SQLite dialect — the first spelling of the dialect
4
+ * contract, not the only conceivable one. Documents are stored JSONB
5
+ * in a BLOB column of a STRICT table; indexed paths become virtual
6
+ * generated columns over `jsonb_extract`; reads render back to text
7
+ * through `json()`. Parameters are positional (`?`) because every
8
+ * binding this package ships binds arrays.
9
+ */
10
+
11
+ import { createDialect } from '../dialect.js';
12
+
13
+ /** @param {string} s */
14
+ function quoteIdentifier(s) {
15
+ return `"${String(s).replace(/"/g, '""')}"`;
16
+ }
17
+
18
+ /** @param {string} s */
19
+ function stringLiteral(s) {
20
+ return `'${String(s).replace(/'/g, "''")}'`;
21
+ }
22
+
23
+ /**
24
+ * SQLite JSON path text from typed segments. Object members are always
25
+ * quoted (`$."name"`), which covers spaces, dots and leading digits; a
26
+ * member name SQLite's path grammar cannot carry (an embedded `"` or a
27
+ * control character) returns `null` so the caller falls back to a
28
+ * whole-document strategy instead of emitting a wrong path.
29
+ * @param {import('../dialect.js').JsonPathSegment[]} segments
30
+ * @returns {string | null}
31
+ */
32
+ function jsonPathText(segments) {
33
+ let text = '$';
34
+ for (const segment of segments) {
35
+ if ('index' in segment) {
36
+ text += `[${segment.index}]`;
37
+ continue;
38
+ }
39
+ // eslint-disable-next-line no-control-regex
40
+ if (/["\u0000-\u001f]/.test(segment.name)) return null;
41
+ text += `."${segment.name}"`;
42
+ }
43
+ return text;
44
+ }
45
+
46
+ /**
47
+ * The declared column type for a schema-declared value type. Under
48
+ * STRICT tables every column needs a type from the strict set; a path
49
+ * whose schema declares none is honestly `ANY`.
50
+ * @param {string | undefined} schemaType
51
+ * @param {string} hint - `'key'` or `'generated'`
52
+ * @returns {string}
53
+ */
54
+ function typeFor(schemaType, hint) {
55
+ switch (schemaType) {
56
+ case 'string': return 'TEXT';
57
+ case 'integer': return 'INTEGER';
58
+ case 'number': return 'REAL';
59
+ case 'boolean': return 'INTEGER';
60
+ default: return hint === 'key' ? 'TEXT' : 'ANY';
61
+ }
62
+ }
63
+
64
+ /**
65
+ * A guarded PRAGMA argument: journal modes and similar keywords are a
66
+ * closed word set, never interpolated user text.
67
+ * @param {string} word
68
+ * @returns {string}
69
+ */
70
+ function pragmaWord(word) {
71
+ if (!/^[a-z_]+$/i.test(String(word)))
72
+ throw new TypeError(`not a PRAGMA keyword: '${word}'`);
73
+ return String(word);
74
+ }
75
+
76
+ export const sqliteDialect = createDialect({
77
+ name: 'sqlite',
78
+ capabilities: {
79
+ jsonb: true,
80
+ generatedColumns: true,
81
+ returning: true,
82
+ upsert: true,
83
+ savepoints: true,
84
+ alterTableFull: false,
85
+ },
86
+ tableSuffix: ' STRICT',
87
+ // RFC 3339 text → epoch milliseconds, in SQL: the migration planner
88
+ // populates derived instant columns with it (rounded to the ms;
89
+ // finer precision is the write contract's business, §10.3)
90
+ epochFromRfc3339: (valueSql) =>
91
+ `CAST(round((julianday(${valueSql}) - 2440587.5) * 86400000.0) AS INTEGER)`,
92
+ docColumnType: 'BLOB',
93
+ quoteIdentifier,
94
+ parameterRef: () => '?',
95
+ stringLiteral,
96
+ booleanLiteral: (b) => (b ? '1' : '0'),
97
+ typeFor,
98
+ limitClause: (limit, offset) => (offset !== undefined && offset > 0
99
+ ? `LIMIT ${limit === null ? -1 : limit} OFFSET ${offset}`
100
+ : `LIMIT ${limit === null ? -1 : limit}`),
101
+ jsonPathText,
102
+ jsonExtract: (columnSql, pathText) =>
103
+ `jsonb_extract(${columnSql}, ${stringLiteral(pathText)})`,
104
+ jsonSet: (exprSql, pathText, valueSql) =>
105
+ `jsonb_set(${exprSql}, ${stringLiteral(pathText)}, ${valueSql})`,
106
+ jsonRemove: (exprSql, pathText) =>
107
+ `jsonb_remove(${exprSql}, ${stringLiteral(pathText)})`,
108
+ jsonAppend: (exprSql, arrayPathText, valueSql) =>
109
+ `jsonb_insert(${exprSql}, ${stringLiteral(`${arrayPathText}[#]`)}, ${valueSql})`,
110
+ jsonEncode: (paramSql) => `jsonb(${paramSql})`,
111
+ jsonText: (columnSql) => `json(${columnSql})`,
112
+ jsonAgg: (exprSql) => `json_group_array(${exprSql})`,
113
+ jsonTypeOf: (columnSql, pathText) =>
114
+ `json_type(${columnSql}, ${stringLiteral(pathText)})`,
115
+ valueTypeOf: (paramSql) => `typeof(${paramSql})`,
116
+ strStartsWith: (valueSql, patternA, patternB) =>
117
+ `substr(${valueSql}, 1, length(${patternA})) = ${patternB}`,
118
+ strEndsWith: (valueSql, patternA, patternB, patternC) =>
119
+ `(length(${patternA}) = 0 OR substr(${valueSql}, -length(${patternB})) = ${patternC})`,
120
+ strContains: (valueSql, patternSql) => `instr(${valueSql}, ${patternSql}) > 0`,
121
+ orderNulls: (nullsFirst) => (nullsFirst ? ' NULLS FIRST' : ' NULLS LAST'),
122
+ rowIdentity: () => '"rowid"',
123
+ explainQuery: (sql) => `EXPLAIN QUERY PLAN ${sql}`,
124
+ excludedRef: (columnSql) => `excluded.${columnSql}`,
125
+ tx: {
126
+ begin: 'BEGIN',
127
+ beginImmediate: 'BEGIN IMMEDIATE',
128
+ commit: 'COMMIT',
129
+ rollback: 'ROLLBACK',
130
+ savepoint: (n) => `SAVEPOINT ${quoteIdentifier(n)}`,
131
+ release: (n) => `RELEASE SAVEPOINT ${quoteIdentifier(n)}`,
132
+ rollbackTo: (n) => `ROLLBACK TO SAVEPOINT ${quoteIdentifier(n)}`,
133
+ },
134
+ pragma: {
135
+ busyTimeout: (ms) => `PRAGMA busy_timeout = ${Math.trunc(ms)}`,
136
+ journalMode: (mode) => `PRAGMA journal_mode = ${pragmaWord(mode)}`,
137
+ foreignKeys: (on) => `PRAGMA foreign_keys = ${on ? 'ON' : 'OFF'}`,
138
+ foreignKeyCheck: () => 'PRAGMA foreign_key_check',
139
+ },
140
+ introspect: {
141
+ version: () => 'SELECT sqlite_version() AS version',
142
+ compileOptions: () =>
143
+ 'SELECT compile_options AS name FROM pragma_compile_options',
144
+ tableExists: () =>
145
+ "SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?",
146
+ columns: (table) =>
147
+ `SELECT name, type, hidden FROM pragma_table_xinfo(${stringLiteral(table)})`,
148
+ indexes: (table) =>
149
+ `SELECT name, "unique" AS uniq, origin FROM pragma_index_list(${stringLiteral(table)})`,
150
+ indexColumns: (index) =>
151
+ `SELECT name FROM pragma_index_info(${stringLiteral(index)})`,
152
+ foreignKeysOn: () => 'SELECT foreign_keys AS enabled FROM pragma_foreign_keys',
153
+ dataVersion: () => 'SELECT data_version AS v FROM pragma_data_version',
154
+ foreignKeyList: (table) =>
155
+ `SELECT "table" AS target, "from" AS source_column, "to" AS target_column, `
156
+ + `on_delete, on_update, seq FROM pragma_foreign_key_list(${stringLiteral(table)}) `
157
+ + 'ORDER BY id, seq',
158
+ // Every schema object one table owns, with the CREATE text SQLite
159
+ // stored verbatim. That text is where the physical facts no pragma
160
+ // reports actually live — STRICT, CHECK, a generated column's
161
+ // expression, a partial index predicate, an index term's collation
162
+ // and direction, the primary key's position — so comparing it against
163
+ // the planned statements is what makes "verify, never alter" true
164
+ // rather than approximately true.
165
+ declaredSql: (table) =>
166
+ 'SELECT type, name, sql FROM sqlite_schema '
167
+ + `WHERE tbl_name = ${stringLiteral(table)} AND sql IS NOT NULL `
168
+ + 'ORDER BY type, name',
169
+ // the whole declared schema, for shape-equality comparison after a
170
+ // rebuild: every object that carries SQL text, in a stable order
171
+ schemaDump: () =>
172
+ "SELECT type, name, tbl_name AS owner, sql FROM sqlite_schema "
173
+ + "WHERE sql IS NOT NULL ORDER BY type, name",
174
+ },
175
+ });