@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.
- package/ARCHITECTURE.md +397 -0
- package/README.md +218 -0
- package/dist/types/algebra.d.ts +133 -0
- package/dist/types/app.d.ts +49 -0
- package/dist/types/capture.d.ts +85 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/dag-job.d.ts +40 -0
- package/dist/types/ddl.d.ts +170 -0
- package/dist/types/dialect.d.ts +130 -0
- package/dist/types/dialects/sqlite.d.ts +9 -0
- package/dist/types/driver.d.ts +128 -0
- package/dist/types/drivers/bun.d.ts +47 -0
- package/dist/types/drivers/node.d.ts +37 -0
- package/dist/types/drivers/wasm.d.ts +65 -0
- package/dist/types/emit-model.d.ts +44 -0
- package/dist/types/emit.d.ts +72 -0
- package/dist/types/entity.d.ts +23 -0
- package/dist/types/errors.d.ts +165 -0
- package/dist/types/graph.d.ts +28 -0
- package/dist/types/index.d.ts +35 -0
- package/dist/types/jobs.d.ts +134 -0
- package/dist/types/live.d.ts +62 -0
- package/dist/types/migrate.d.ts +163 -0
- package/dist/types/model.d.ts +36 -0
- package/dist/types/patch-sql.d.ts +37 -0
- package/dist/types/plan.d.ts +119 -0
- package/dist/types/profile.d.ts +80 -0
- package/dist/types/query.d.ts +100 -0
- package/dist/types/residual.d.ts +50 -0
- package/dist/types/store.d.ts +53 -0
- package/dist/types/tracker.d.ts +43 -0
- package/dist/types/typed.d.ts +15 -0
- package/dist/types/types.d.ts +26 -0
- package/dist/types/udf.d.ts +70 -0
- package/dist/types/window.d.ts +52 -0
- package/docs/JOBS-FORMAT.md +218 -0
- package/docs/LIVE-FORMAT.md +348 -0
- package/docs/MIGRATION-FORMAT.md +302 -0
- package/docs/MODEL-FORMAT.md +928 -0
- package/package.json +81 -0
- package/schemas/jaren-migration.draft-07.schema.json +144 -0
- package/schemas/jaren-migration.schema.json +144 -0
- package/schemas/jaren-model.draft-07.schema.json +149 -0
- package/schemas/jaren-model.schema.json +149 -0
- package/src/algebra.js +105 -0
- package/src/app.js +108 -0
- package/src/capture.js +584 -0
- package/src/cli.js +264 -0
- package/src/dag-job.js +86 -0
- package/src/ddl.js +588 -0
- package/src/dialect.js +297 -0
- package/src/dialects/sqlite.js +175 -0
- package/src/driver.js +419 -0
- package/src/drivers/bun.js +101 -0
- package/src/drivers/node.js +93 -0
- package/src/drivers/wasm.js +178 -0
- package/src/emit-model.js +208 -0
- package/src/emit.js +393 -0
- package/src/entity.js +367 -0
- package/src/errors.js +173 -0
- package/src/graph.js +101 -0
- package/src/index.js +64 -0
- package/src/jobs.js +507 -0
- package/src/live.js +899 -0
- package/src/migrate.js +1411 -0
- package/src/model.js +476 -0
- package/src/patch-sql.js +150 -0
- package/src/plan.js +1038 -0
- package/src/profile.js +131 -0
- package/src/query.js +1010 -0
- package/src/residual.js +91 -0
- package/src/store.js +1422 -0
- package/src/tracker.js +776 -0
- package/src/typed.js +19 -0
- package/src/types.js +36 -0
- package/src/udf.js +132 -0
- package/src/window.js +125 -0
- package/types/app.d.ts +36 -0
- package/types/bun.d.ts +9 -0
- package/types/index.d.ts +592 -0
- package/types/node.d.ts +15 -0
- package/types/typed.d.ts +108 -0
- 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
|
+
});
|