@jarenjs/db 0.49.2 → 0.66.1
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 +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/src/dialect.js
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* concatenates SQL; that costs one indirection now, and without it a
|
|
9
9
|
* second backend is a rewrite.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* connection
|
|
11
|
+
* A dialect answers a CLOSED capability set of its own
|
|
12
|
+
* ({@link DIALECT_CAPABILITIES}), and every one of those answers is
|
|
13
|
+
* SYNTACTIC — what SQL this engine will accept. Deliberately NOT here,
|
|
14
|
+
* because they are behavioural rather than syntactic: whether functions
|
|
15
|
+
* can be registered on THIS connection, whether the library was built
|
|
16
|
+
* with the R\*Tree module, whether change capture exists and in what
|
|
17
|
+
* form. Those are the connection's, probed once at open.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -23,6 +25,92 @@
|
|
|
23
25
|
* @typedef {{ name: string } | { index: number }} JsonPathSegment
|
|
24
26
|
*/
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The closed set of dialect capabilities, each with the answer a
|
|
30
|
+
* spelling spec that says nothing gets. They are SYNTACTIC — what SQL
|
|
31
|
+
* this engine will accept — as opposed to the connection capabilities
|
|
32
|
+
* {@link module:driver}'s probe reports, which are what a particular
|
|
33
|
+
* library and build can DO.
|
|
34
|
+
*
|
|
35
|
+
* Closed, and defaulted to the conservative answer, for one reason: a
|
|
36
|
+
* misspelled capability must not read as a quiet `false` on the dialect
|
|
37
|
+
* that has the feature, nor as a quiet `true` on the one that does not.
|
|
38
|
+
* {@link createDialect} refuses a name outside this set.
|
|
39
|
+
*/
|
|
40
|
+
export const DIALECT_CAPABILITIES = Object.freeze({
|
|
41
|
+
/** A binary JSON storage type distinct from text. */
|
|
42
|
+
jsonb: false,
|
|
43
|
+
/** Columns whose value is an expression over another column. */
|
|
44
|
+
generatedColumns: false,
|
|
45
|
+
/** A generated column may be indexed. PostgreSQL 18's VIRTUAL ones
|
|
46
|
+
* may not, which is why storage and indexability are two answers. */
|
|
47
|
+
indexableGeneratedColumns: false,
|
|
48
|
+
/** A column may be declared with no scalar type — SQLite's `ANY`.
|
|
49
|
+
* Where this is false, an indexed path the schema does not type has
|
|
50
|
+
* no honest column type and the planner keeps it in the residual. */
|
|
51
|
+
untypedColumns: false,
|
|
52
|
+
/** `INSERT ... RETURNING`. */
|
|
53
|
+
returning: false,
|
|
54
|
+
/** `INSERT ... ON CONFLICT (key) DO UPDATE`. */
|
|
55
|
+
upsert: false,
|
|
56
|
+
/** `SAVEPOINT` / `RELEASE` / `ROLLBACK TO`. */
|
|
57
|
+
savepoints: false,
|
|
58
|
+
/** A `SAVEPOINT` outside a transaction STARTS one. SQLite's does,
|
|
59
|
+
* which is why a top-level transaction there can be one checkpoint;
|
|
60
|
+
* PostgreSQL refuses a savepoint outside a transaction block, so a
|
|
61
|
+
* top-level transaction has to be `BEGIN` and `COMMIT` there. */
|
|
62
|
+
savepointStartsTransaction: false,
|
|
63
|
+
/** A transaction can take the write lock up front (`tx.beginImmediate`
|
|
64
|
+
* is a distinct statement rather than a synonym for `tx.begin`). */
|
|
65
|
+
immediateTransactions: false,
|
|
66
|
+
/** A `GROUP BY` / `ORDER BY` term may name a result alias, so a
|
|
67
|
+
* bucket ladder is written once rather than three times. */
|
|
68
|
+
groupByAlias: false,
|
|
69
|
+
/** `ALTER TABLE` can restructure in place (drop a constraint, retype
|
|
70
|
+
* a column) rather than only add and drop columns. */
|
|
71
|
+
alterTableFull: false,
|
|
72
|
+
/** `CREATE VIRTUAL TABLE` — the second physical realization of a
|
|
73
|
+
* `derive: 'bbox'` column set. */
|
|
74
|
+
virtualTables: false,
|
|
75
|
+
/** Row triggers, which the virtual-table mapping is kept in sync by. */
|
|
76
|
+
triggers: false,
|
|
77
|
+
/** A closed configuration vocabulary applied per connection — the
|
|
78
|
+
* `pragma` group. Where this is false the store applies none and the
|
|
79
|
+
* effective record is empty. */
|
|
80
|
+
pragmas: false,
|
|
81
|
+
/** The engine stores each schema object's CREATE text verbatim, so a
|
|
82
|
+
* declared-text comparison is available to the drift check. Where it
|
|
83
|
+
* is false the drift check is the structural one (columns, indexes,
|
|
84
|
+
* foreign keys) and says so. */
|
|
85
|
+
declaredSqlText: false,
|
|
86
|
+
/** Referential integrity is always enforced, so no per-connection
|
|
87
|
+
* switch is set or verified at open. */
|
|
88
|
+
foreignKeysAlwaysOn: false,
|
|
89
|
+
/** A per-row identity that reproduces INSERTION order — SQLite's
|
|
90
|
+
* `rowid`, or a declared identity column this dialect adds to every
|
|
91
|
+
* table it creates. A collection is a sequence; without one, its
|
|
92
|
+
* order is whatever the engine answers in. */
|
|
93
|
+
rowIdentity: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Merge a spec's declared capabilities onto the closed defaults,
|
|
98
|
+
* refusing a name outside the set.
|
|
99
|
+
* @param {Record<string, any>} declared
|
|
100
|
+
* @returns {Readonly<Record<string, boolean>>}
|
|
101
|
+
*/
|
|
102
|
+
function normalizeCapabilities(declared) {
|
|
103
|
+
const out = { ...DIALECT_CAPABILITIES };
|
|
104
|
+
for (const [name, value] of Object.entries(declared ?? {})) {
|
|
105
|
+
if (!Object.hasOwn(DIALECT_CAPABILITIES, name)) {
|
|
106
|
+
throw new TypeError(`createDialect: '${name}' is not a dialect capability; the set is `
|
|
107
|
+
+ Object.keys(DIALECT_CAPABILITIES).map((n) => `'${n}'`).join(', '));
|
|
108
|
+
}
|
|
109
|
+
out[name] = value === true;
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze(out);
|
|
112
|
+
}
|
|
113
|
+
|
|
26
114
|
/**
|
|
27
115
|
* Compose a dialect from its spelling spec. Every statement the store
|
|
28
116
|
* ever runs is built here from the spec's primitives, so a spec with
|
|
@@ -40,9 +128,15 @@
|
|
|
40
128
|
* stringLiteral: (s: string) => string,
|
|
41
129
|
* booleanLiteral: (b: boolean) => string,
|
|
42
130
|
* typeFor: (schemaType: string | undefined, hint: string) => string,
|
|
131
|
+
* autoKeyType?: string,
|
|
132
|
+
* comparableColumnType?: (declaredType: string) => string,
|
|
133
|
+
* columnUsableFor?: (declaredType: string | undefined, kind: string) => boolean,
|
|
134
|
+
* generatedStorage: string,
|
|
135
|
+
* identityColumn?: { name: string, type: string },
|
|
136
|
+
* epochFromRfc3339: (valueSql: string) => string,
|
|
43
137
|
* limitClause: (limit: number, offset?: number) => string,
|
|
44
138
|
* jsonPathText: (segments: JsonPathSegment[]) => string | null,
|
|
45
|
-
* jsonExtract: (columnSql: string, pathText: string) => string,
|
|
139
|
+
* jsonExtract: (columnSql: string, pathText: string, kind?: string) => string,
|
|
46
140
|
* derivedExpression?: (memberSql: string, column: { derive: string,
|
|
47
141
|
* precision?: number, component?: string, dims?: number }) => string,
|
|
48
142
|
* jsonSet: (exprSql: string, pathText: string, valueSql: string) => string,
|
|
@@ -52,6 +146,12 @@
|
|
|
52
146
|
* jsonText: (columnSql: string) => string,
|
|
53
147
|
* jsonAgg: (exprSql: string) => string,
|
|
54
148
|
* jsonTypeOf: (columnSql: string, pathText: string) => string,
|
|
149
|
+
* numericTypeNames: readonly string[],
|
|
150
|
+
* jsonObject: (pairsSql: string) => string,
|
|
151
|
+
* jsonEmbed: (columnSql: string) => string,
|
|
152
|
+
* externalEncoding?: 'value' | 'json',
|
|
153
|
+
* externalCompare?: (valueSql: string, kind: string) => string,
|
|
154
|
+
* externalRef?: (paramSql: string, kind: string) => string,
|
|
55
155
|
* valueTypeOf: (paramSql: string) => string,
|
|
56
156
|
* strStartsWith: (valueSql: string, lowerParamSql: string, upperParamSql: string) => string,
|
|
57
157
|
* strStartsWithExact: (valueSql: string, patternA: string, patternB: string) => string,
|
|
@@ -64,16 +164,28 @@
|
|
|
64
164
|
* rowIdentity: () => string,
|
|
65
165
|
* identityIn: (identitySql: string, paramSqls: string[]) => string,
|
|
66
166
|
* rtree?: { module: string, columns: readonly string[] },
|
|
167
|
+
* rtreeDdl?: Record<string, Function>,
|
|
67
168
|
* explainQuery: (sql: string) => string,
|
|
169
|
+
* explainLines: (rows: any[]) => string[],
|
|
170
|
+
* isFullScan: (line: string, tables: readonly string[]) => boolean,
|
|
171
|
+
* usesIndex: (line: string, index: string) => boolean,
|
|
68
172
|
* excludedRef: (columnSql: string) => string,
|
|
69
173
|
* tx: { begin: string, beginImmediate: string, commit: string,
|
|
70
174
|
* rollback: string,
|
|
71
175
|
* savepoint: (n: string) => string, release: (n: string) => string,
|
|
72
176
|
* rollbackTo: (n: string) => string },
|
|
73
|
-
* pragma
|
|
74
|
-
*
|
|
75
|
-
*
|
|
177
|
+
* pragma?: { set: (name: string, value: number | string) => string,
|
|
178
|
+
* foreignKeys: (on: boolean) => string,
|
|
179
|
+
* foreignKeyCheck: () => string,
|
|
180
|
+
* walCheckpoint: (mode: string) => string,
|
|
181
|
+
* integrityCheck: (limit?: number) => string,
|
|
182
|
+
* optimize: () => string },
|
|
183
|
+
* schemaTypeOf?: (declaredType: string) => string | undefined,
|
|
184
|
+
* memberPathOf?: (expression: string) => (JsonPathSegment[] | null),
|
|
185
|
+
* expressionOf?: (expression: string, byName: Record<string, string>) => (any | null),
|
|
186
|
+
* readGenerated?: (rows: any[]) => { name: string, expression: string }[],
|
|
76
187
|
* introspect: { version: () => string, compileOptions: () => string,
|
|
188
|
+
* pragma: (name: string) => string,
|
|
77
189
|
* tableExists: () => string, columns: (table: string) => string,
|
|
78
190
|
* indexes: (table: string) => string,
|
|
79
191
|
* indexColumns: (index: string) => string,
|
|
@@ -102,11 +214,45 @@ export function createDialect(spec) {
|
|
|
102
214
|
const generatedColumnSql = (docColumn, column) => {
|
|
103
215
|
if (column.stored === true) return `${q(column.name)} ${column.type}`;
|
|
104
216
|
const expression = column.expression
|
|
105
|
-
?? spec.jsonExtract(q(docColumn), column.pathText);
|
|
106
|
-
return `${q(column.name)} ${column.type} GENERATED ALWAYS AS (${expression})
|
|
217
|
+
?? spec.jsonExtract(q(docColumn), column.pathText, column.kind);
|
|
218
|
+
return `${q(column.name)} ${column.type} GENERATED ALWAYS AS (${expression}) `
|
|
219
|
+
+ spec.generatedStorage;
|
|
107
220
|
};
|
|
108
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The identity column this dialect adds to every table it creates, or
|
|
224
|
+
* `null`. An engine with no per-row identity of its own declares one
|
|
225
|
+
* here rather than losing the collection's INSERTION order: the store
|
|
226
|
+
* orders by `rowIdentity()` wherever the model says "the sequence",
|
|
227
|
+
* and an engine that answers rows in whatever order an index scan
|
|
228
|
+
* produced is not answering the same question. Declared, so the shape
|
|
229
|
+
* check sees it and introspection can tell it from a model member.
|
|
230
|
+
* @returns {string[]} zero or one column definition
|
|
231
|
+
*/
|
|
232
|
+
const identityColumnSql = () => (spec.identityColumn === undefined
|
|
233
|
+
? []
|
|
234
|
+
: [`${q(spec.identityColumn.name)} ${spec.identityColumn.type}`]);
|
|
235
|
+
|
|
109
236
|
const ddl = Object.freeze({
|
|
237
|
+
// the R*Tree mapping's own statements, when this spelling spec
|
|
238
|
+
// carries one; a dialect without `capabilities.virtualTables`
|
|
239
|
+
// composes none of them and the planner never asks
|
|
240
|
+
...(spec.rtreeDdl ?? {}),
|
|
241
|
+
/**
|
|
242
|
+
* The idempotent form of a CREATE statement this dialect emitted —
|
|
243
|
+
* what the open path runs: two processes racing to create one fresh
|
|
244
|
+
* file both probe "absent", and the loser's CREATE must be a no-op
|
|
245
|
+
* rather than a failure. Only the open path takes it; a migration's
|
|
246
|
+
* planned DDL keeps its exact text. Shape verification still runs on
|
|
247
|
+
* the row the probe found, so an existing table is never silently
|
|
248
|
+
* accepted.
|
|
249
|
+
* @param {string} sql - a statement one of the builders below emitted
|
|
250
|
+
* @returns {string}
|
|
251
|
+
*/
|
|
252
|
+
idempotent(sql) {
|
|
253
|
+
return sql.replace(/^CREATE (TABLE|UNIQUE INDEX|INDEX|VIRTUAL TABLE|TRIGGER) /,
|
|
254
|
+
'CREATE $1 IF NOT EXISTS ');
|
|
255
|
+
},
|
|
110
256
|
/**
|
|
111
257
|
* One collection's physical table: a key column, the JSON document
|
|
112
258
|
* column, and a virtual generated column per indexed path.
|
|
@@ -119,6 +265,7 @@ export function createDialect(spec) {
|
|
|
119
265
|
const columns = [
|
|
120
266
|
`${q(keyColumn)} ${keyType} PRIMARY KEY`,
|
|
121
267
|
`${q(docColumn)} ${spec.docColumnType} NOT NULL`,
|
|
268
|
+
...identityColumnSql(),
|
|
122
269
|
...generated.map((g) => generatedColumnSql(docColumn, g)),
|
|
123
270
|
];
|
|
124
271
|
return `CREATE TABLE ${q(table)} (${columns.join(', ')})${spec.tableSuffix}`;
|
|
@@ -175,104 +322,6 @@ export function createDialect(spec) {
|
|
|
175
322
|
dropIndex(name) {
|
|
176
323
|
return `DROP INDEX ${q(name)}`;
|
|
177
324
|
},
|
|
178
|
-
/**
|
|
179
|
-
* The second physical realization of a `derive: 'bbox'` column set
|
|
180
|
-
* (MODEL-FORMAT §2.1, `physical: 'rtree'`): an R\*Tree virtual
|
|
181
|
-
* table beside the collection, keyed by the collection's row id and
|
|
182
|
-
* carrying the four box edges as `(minx, maxx, miny, maxy)`.
|
|
183
|
-
*
|
|
184
|
-
* The coordinates are 32-bit floats rounded OUTWARD, so the stored
|
|
185
|
-
* box is a superset of the row's — no false negatives, which is
|
|
186
|
-
* what an implied conjunct needs, and the reason `$bbox-intersects`
|
|
187
|
-
* stops being exact under this mapping.
|
|
188
|
-
* @param {{ name: string }} shape
|
|
189
|
-
* @returns {string}
|
|
190
|
-
*/
|
|
191
|
-
createVirtualTable({ name }) {
|
|
192
|
-
return `CREATE VIRTUAL TABLE ${q(name)} USING ${spec.rtree.module}(`
|
|
193
|
-
+ `${spec.rtree.columns.map(q).join(', ')})`;
|
|
194
|
-
},
|
|
195
|
-
/**
|
|
196
|
-
* @param {string} name
|
|
197
|
-
* @returns {string}
|
|
198
|
-
*/
|
|
199
|
-
dropVirtualTable(name) {
|
|
200
|
-
return `DROP TABLE ${q(name)}`;
|
|
201
|
-
},
|
|
202
|
-
/**
|
|
203
|
-
* @param {string} name
|
|
204
|
-
* @returns {string}
|
|
205
|
-
*/
|
|
206
|
-
dropTrigger(name) {
|
|
207
|
-
return `DROP TRIGGER ${q(name)}`;
|
|
208
|
-
},
|
|
209
|
-
/**
|
|
210
|
-
* Fill an R\*Tree from the documents already stored — the migration
|
|
211
|
-
* step that turns a `columns` collection into an `rtree` one. The
|
|
212
|
-
* `IS NOT NULL` is §3.2's rule in SQL: a row with no bounded
|
|
213
|
-
* position is ABSENT from the index, not at `[0, 0]`.
|
|
214
|
-
* @param {{ table: string, virtualTable: string,
|
|
215
|
-
* edges: { name: string }[] }} shape
|
|
216
|
-
* @returns {string}
|
|
217
|
-
*/
|
|
218
|
-
fillVirtualTable({ table, virtualTable, edges }) {
|
|
219
|
-
const columns = spec.rtree.columns;
|
|
220
|
-
const sources = [spec.rowIdentity(), ...edges.map((edge) => q(edge.name))];
|
|
221
|
-
return `INSERT INTO ${q(virtualTable)} (${columns.map(q).join(', ')}) `
|
|
222
|
-
+ `SELECT ${sources.join(', ')} FROM ${q(table)} `
|
|
223
|
-
+ `WHERE ${q(edges[0].name)} IS NOT NULL`;
|
|
224
|
-
},
|
|
225
|
-
/**
|
|
226
|
-
* The three triggers that keep an R\*Tree in sync with its
|
|
227
|
-
* collection — insert, update, delete — as DECLARED objects of the
|
|
228
|
-
* collection table.
|
|
229
|
-
*
|
|
230
|
-
* Declared, and not a second write path in JavaScript: a trigger is
|
|
231
|
-
* inside the writing transaction by construction (SQLite cannot
|
|
232
|
-
* separate them), no write path can bypass it (`insert`,
|
|
233
|
-
* `insertAllocated`, `upsert`, a translated patch, the patch
|
|
234
|
-
* fallback, a delete and a migration backfill all fire it), and it
|
|
235
|
-
* belongs to the collection table, so the existing declared-text
|
|
236
|
-
* drift check sees it for free.
|
|
237
|
-
*
|
|
238
|
-
* The body reads the DERIVED COLUMNS through `NEW` rather than
|
|
239
|
-
* restating the box expression, so the columns stay the box's one
|
|
240
|
-
* definition — and that text works unchanged on the stored-column
|
|
241
|
-
* branch, where those columns are ordinary ones.
|
|
242
|
-
*
|
|
243
|
-
* The `IS NOT NULL` guard is load-bearing: an R\*Tree coerces a
|
|
244
|
-
* `NULL` coordinate to `0.0` without complaint, so without it every
|
|
245
|
-
* unbounded document would land on Null Island instead of being
|
|
246
|
-
* absent (MODEL-FORMAT §3.2).
|
|
247
|
-
* @param {{ table: string, virtualTable: string, prefix: string,
|
|
248
|
-
* edges: { name: string }[] }} shape - `edges` are the four
|
|
249
|
-
* derived columns in `(w, e, s, n)` order, which is the order the
|
|
250
|
-
* virtual table's `(minx, maxx, miny, maxy)` carry
|
|
251
|
-
* @returns {{ name: string, sql: string }[]}
|
|
252
|
-
*/
|
|
253
|
-
createSyncTriggers({ table, virtualTable, prefix, edges }) {
|
|
254
|
-
const rid = spec.rowIdentity();
|
|
255
|
-
const target = `${q(virtualTable)} (${spec.rtree.columns.map(q).join(', ')})`;
|
|
256
|
-
const guard = `${q(edges[0].name)} IS NOT NULL`;
|
|
257
|
-
const values = (row) => [`${row}.${rid}`,
|
|
258
|
-
...edges.map((edge) => `${row}.${q(edge.name)}`)].join(', ');
|
|
259
|
-
return [
|
|
260
|
-
{ name: `${prefix}_ai`,
|
|
261
|
-
sql: `CREATE TRIGGER ${q(`${prefix}_ai`)} AFTER INSERT ON ${q(table)} `
|
|
262
|
-
+ `WHEN NEW.${guard} BEGIN `
|
|
263
|
-
+ `INSERT INTO ${target} VALUES (${values('NEW')}); END` },
|
|
264
|
-
// one trigger, not two: the old id leaves and the new box
|
|
265
|
-
// arrives only when it exists, so a document that loses its
|
|
266
|
-
// geometry leaves the index rather than keeping a stale box
|
|
267
|
-
{ name: `${prefix}_au`,
|
|
268
|
-
sql: `CREATE TRIGGER ${q(`${prefix}_au`)} AFTER UPDATE ON ${q(table)} BEGIN `
|
|
269
|
-
+ `DELETE FROM ${q(virtualTable)} WHERE ${q(spec.rtree.columns[0])} = OLD.${rid}; `
|
|
270
|
-
+ `INSERT INTO ${target} SELECT ${values('NEW')} WHERE NEW.${guard}; END` },
|
|
271
|
-
{ name: `${prefix}_ad`,
|
|
272
|
-
sql: `CREATE TRIGGER ${q(`${prefix}_ad`)} AFTER DELETE ON ${q(table)} BEGIN `
|
|
273
|
-
+ `DELETE FROM ${q(virtualTable)} WHERE ${q(spec.rtree.columns[0])} = OLD.${rid}; END` },
|
|
274
|
-
];
|
|
275
|
-
},
|
|
276
325
|
/**
|
|
277
326
|
* @param {string} table
|
|
278
327
|
* @returns {string}
|
|
@@ -323,6 +372,7 @@ export function createDialect(spec) {
|
|
|
323
372
|
}
|
|
324
373
|
return sql;
|
|
325
374
|
});
|
|
375
|
+
rendered.push(...identityColumnSql());
|
|
326
376
|
if (compositeKey !== undefined && compositeKey.length > 0)
|
|
327
377
|
rendered.push(`PRIMARY KEY (${compositeKey.map(q).join(', ')})`);
|
|
328
378
|
return `CREATE TABLE ${q(table)} (${rendered.join(', ')})${spec.tableSuffix}`;
|
|
@@ -334,8 +384,9 @@ export function createDialect(spec) {
|
|
|
334
384
|
* @returns {string}
|
|
335
385
|
*/
|
|
336
386
|
createPlainTable({ table, columns }) {
|
|
337
|
-
const rendered = columns.map((column) =>
|
|
338
|
-
`${q(column.name)} ${column.type}${column.primaryKey === true ? ' PRIMARY KEY' : ''}`)
|
|
387
|
+
const rendered = [...columns.map((column) =>
|
|
388
|
+
`${q(column.name)} ${column.type}${column.primaryKey === true ? ' PRIMARY KEY' : ''}`),
|
|
389
|
+
...identityColumnSql()];
|
|
339
390
|
return `CREATE TABLE IF NOT EXISTS ${q(table)} (${rendered.join(', ')})${spec.tableSuffix}`;
|
|
340
391
|
},
|
|
341
392
|
});
|
|
@@ -439,7 +490,7 @@ export function createDialect(spec) {
|
|
|
439
490
|
|
|
440
491
|
return Object.freeze({
|
|
441
492
|
name: spec.name,
|
|
442
|
-
capabilities:
|
|
493
|
+
capabilities: normalizeCapabilities(spec.capabilities),
|
|
443
494
|
docColumnType: spec.docColumnType,
|
|
444
495
|
// the declared type of a `derive: 'vector'` column — the bytes of
|
|
445
496
|
// the packed form, spelled by the dialect like every other type
|
|
@@ -449,6 +500,35 @@ export function createDialect(spec) {
|
|
|
449
500
|
stringLiteral: spec.stringLiteral,
|
|
450
501
|
booleanLiteral: spec.booleanLiteral,
|
|
451
502
|
typeFor: spec.typeFor,
|
|
503
|
+
/**
|
|
504
|
+
* The declared type of a key the DATABASE allocates
|
|
505
|
+
* (`identity: 'integer'`), when that is not simply the integer type
|
|
506
|
+
* — an engine whose auto-allocation is a column property rather
|
|
507
|
+
* than a consequence of the key's type spells it here.
|
|
508
|
+
*/
|
|
509
|
+
autoKeyType: spec.autoKeyType,
|
|
510
|
+
/**
|
|
511
|
+
* One declared column type reduced to the form the catalog reports
|
|
512
|
+
* it as, so the shape check compares like with like: an auto-key's
|
|
513
|
+
* clause, a collation, a width the engine normalizes away. Applied
|
|
514
|
+
* to BOTH sides of the comparison.
|
|
515
|
+
*/
|
|
516
|
+
comparableColumnType: spec.comparableColumnType
|
|
517
|
+
?? ((declaredType) => String(declaredType).toUpperCase()),
|
|
518
|
+
/**
|
|
519
|
+
* Whether a generated column over a member the schema types
|
|
520
|
+
* `declaredType` can be compared against a value of `kind`
|
|
521
|
+
* (`'text'`, `'number'`, `'boolean'`). On an engine whose columns
|
|
522
|
+
* carry a real SQL type, comparing a `text` column to a number is a
|
|
523
|
+
* type error rather than a false row, so the emitter reads the
|
|
524
|
+
* member out of the document instead — the same answer, unindexed.
|
|
525
|
+
*/
|
|
526
|
+
columnUsableFor: spec.columnUsableFor ?? (() => true),
|
|
527
|
+
/** The storage word a generated column is declared with. */
|
|
528
|
+
generatedStorage: spec.generatedStorage,
|
|
529
|
+
/** The identity column added to every created table, or undefined. */
|
|
530
|
+
identityColumn: spec.identityColumn === undefined
|
|
531
|
+
? undefined : Object.freeze({ ...spec.identityColumn }),
|
|
452
532
|
limitClause: spec.limitClause,
|
|
453
533
|
jsonPathText: spec.jsonPathText,
|
|
454
534
|
jsonExtract: spec.jsonExtract,
|
|
@@ -470,6 +550,35 @@ export function createDialect(spec) {
|
|
|
470
550
|
jsonText: spec.jsonText,
|
|
471
551
|
jsonAgg: spec.jsonAgg,
|
|
472
552
|
jsonTypeOf: spec.jsonTypeOf,
|
|
553
|
+
/**
|
|
554
|
+
* The spellings {@link jsonTypeOf} answers for a JSON NUMBER. SQLite
|
|
555
|
+
* discriminates the two it stores (`integer`, `real`); an engine with
|
|
556
|
+
* one JSON number type answers one name. Everything else in the
|
|
557
|
+
* vocabulary is shared — `text`, `true`, `false`, `null`, `object`,
|
|
558
|
+
* `array` — because the row decoder reads those names in JavaScript.
|
|
559
|
+
*/
|
|
560
|
+
numericTypeNames: Object.freeze([...spec.numericTypeNames]),
|
|
561
|
+
/** An object built from alternating name/value SQL — the shape a
|
|
562
|
+
* graph load's nested include answers. */
|
|
563
|
+
jsonObject: spec.jsonObject,
|
|
564
|
+
/** The document column as a NESTED JSON value rather than as text:
|
|
565
|
+
* what an enclosing object embeds, as opposed to what a caller
|
|
566
|
+
* reads back. On an engine with one JSON type the two differ. */
|
|
567
|
+
jsonEmbed: spec.jsonEmbed,
|
|
568
|
+
/**
|
|
569
|
+
* How an EXTERNAL operand's value reaches a statement. `'value'`
|
|
570
|
+
* binds it as itself — a dynamically typed engine compares it with
|
|
571
|
+
* whatever the member holds. `'json'` binds its JSON encoding,
|
|
572
|
+
* because a statically typed engine cannot bind one placeholder
|
|
573
|
+
* against a text member in one branch and a numeric member in
|
|
574
|
+
* another: the parameter's own type is fixed when it is bound, and
|
|
575
|
+
* a guard the row would fail does not stop the coercion.
|
|
576
|
+
*/
|
|
577
|
+
externalEncoding: spec.externalEncoding ?? 'value',
|
|
578
|
+
/** The MEMBER side of an external comparison, at the branch's kind. */
|
|
579
|
+
externalCompare: spec.externalCompare ?? ((valueSql) => valueSql),
|
|
580
|
+
/** The PARAMETER side of an external comparison, at the same kind. */
|
|
581
|
+
externalRef: spec.externalRef ?? ((paramSql) => paramSql),
|
|
473
582
|
valueTypeOf: spec.valueTypeOf,
|
|
474
583
|
strStartsWith: spec.strStartsWith,
|
|
475
584
|
strStartsWithExact: spec.strStartsWithExact,
|
|
@@ -487,6 +596,9 @@ export function createDialect(spec) {
|
|
|
487
596
|
/** One grouped aggregate; `null` counts ROWS rather than values. */
|
|
488
597
|
groupAggregate: spec.groupAggregate,
|
|
489
598
|
rowIdentity: spec.rowIdentity,
|
|
599
|
+
/** Membership of the row identity in a bound list — the fetch of a
|
|
600
|
+
* k-nearest plan's candidates after the engine's cut. */
|
|
601
|
+
identityIn: spec.identityIn,
|
|
490
602
|
/**
|
|
491
603
|
* The R\*Tree spelling: the module name and the virtual table's own
|
|
492
604
|
* column list, in `(id, minx, maxx, miny, maxy)` order. Read only
|
|
@@ -494,7 +606,50 @@ export function createDialect(spec) {
|
|
|
494
606
|
* spec that omits it simply cannot carry that mapping.
|
|
495
607
|
*/
|
|
496
608
|
rtree: Object.freeze({ ...spec.rtree }),
|
|
609
|
+
/**
|
|
610
|
+
* The schema type a declared column type came from — the inverse of
|
|
611
|
+
* {@link typeFor}, as far as one exists. `undefined` where the
|
|
612
|
+
* column carries no scalar type, and where a mapping is lossy the
|
|
613
|
+
* introspector says so rather than guessing: two schema types that
|
|
614
|
+
* share a column type cannot be told apart on the way back.
|
|
615
|
+
*/
|
|
616
|
+
schemaTypeOf: spec.schemaTypeOf,
|
|
617
|
+
/**
|
|
618
|
+
* The member path a GENERATED column's expression reads, recovered
|
|
619
|
+
* from the dialect's own spelling of it. `null` for an expression
|
|
620
|
+
* this dialect did not write — which is a loss the introspector
|
|
621
|
+
* reports rather than a path it invents.
|
|
622
|
+
*/
|
|
623
|
+
memberPathOf: spec.memberPathOf,
|
|
624
|
+
/**
|
|
625
|
+
* The declared index EXPRESSION a generated column's SQL computes,
|
|
626
|
+
* recovered from the dialect's own spelling of it. `byName` maps the
|
|
627
|
+
* engine's function name back to the model's — the same name where
|
|
628
|
+
* the store registered it, the host's `sql` name where the engine
|
|
629
|
+
* has its own. `null` for anything this dialect did not write, which
|
|
630
|
+
* the introspector reports rather than guesses.
|
|
631
|
+
*/
|
|
632
|
+
expressionOf: spec.expressionOf,
|
|
633
|
+
/**
|
|
634
|
+
* The `{ name, expression }` pairs behind `introspect.generated`'s
|
|
635
|
+
* rows. Two engines answer that question with different row shapes
|
|
636
|
+
* — one hands back a catalog column, the other the table's whole
|
|
637
|
+
* CREATE text — and the dialect that wrote the expression is the
|
|
638
|
+
* one that can read it.
|
|
639
|
+
*/
|
|
640
|
+
readGenerated: spec.readGenerated,
|
|
497
641
|
explainQuery: spec.explainQuery,
|
|
642
|
+
/** The plan narrative, one line per row the engine answered. */
|
|
643
|
+
explainLines: spec.explainLines,
|
|
644
|
+
/**
|
|
645
|
+
* Whether one narrative line reports a FULL TABLE SCAN of one of
|
|
646
|
+
* the named tables (or of an alias a join statement gave one). The
|
|
647
|
+
* safe profile's scan refusal is verified against the database's own
|
|
648
|
+
* plan, and only the dialect can read that engine's words.
|
|
649
|
+
*/
|
|
650
|
+
isFullScan: spec.isFullScan,
|
|
651
|
+
/** Whether one narrative line reports a seek through the named index. */
|
|
652
|
+
usesIndex: spec.usesIndex,
|
|
498
653
|
excludedRef: spec.excludedRef,
|
|
499
654
|
epochFromRfc3339: spec.epochFromRfc3339,
|
|
500
655
|
tx: Object.freeze({ ...spec.tx }),
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Reading an index expression back out of the SQL a dialect wrote
|
|
4
|
+
* for it — the inverse of `expressionSql`, and the reason introspection
|
|
5
|
+
* can round-trip one.
|
|
6
|
+
*
|
|
7
|
+
* The grammar is tiny and closed, because the emitter's is: a call is
|
|
8
|
+
* `name(arg, …)`, an argument is another call, a member read, or a
|
|
9
|
+
* literal. What differs between engines is only how a member and a
|
|
10
|
+
* function name are SPELLED, so both pass those two in and share
|
|
11
|
+
* everything else — a second copy of this walk is a second place for
|
|
12
|
+
* the two halves to drift apart.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} sql
|
|
17
|
+
* @param {{ memberOf: (text: string) => (any | null),
|
|
18
|
+
* nameOf: (text: string) => (string | null),
|
|
19
|
+
* stringOf: (text: string) => (string | null) }} spelling
|
|
20
|
+
* @returns {any | null} the expression, or `null` for SQL this dialect
|
|
21
|
+
* did not write
|
|
22
|
+
*/
|
|
23
|
+
export function readExpression(sql, spelling) {
|
|
24
|
+
const text = String(sql).trim();
|
|
25
|
+
const parsed = parseNode(text, spelling);
|
|
26
|
+
return parsed === null || parsed.rest.trim().length > 0 ? null : parsed.node;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One node, and whatever follows it.
|
|
31
|
+
* @param {string} text
|
|
32
|
+
* @param {any} spelling
|
|
33
|
+
* @returns {{ node: any, rest: string } | null}
|
|
34
|
+
*/
|
|
35
|
+
function parseNode(text, spelling) {
|
|
36
|
+
let source = text.trimStart();
|
|
37
|
+
// a parenthesised node: the emitter brackets a member read, and an
|
|
38
|
+
// engine that re-renders adds its own
|
|
39
|
+
if (source.startsWith('(')) {
|
|
40
|
+
const inner = balanced(source);
|
|
41
|
+
if (inner === null) return null;
|
|
42
|
+
const member = spelling.memberOf(inner.body);
|
|
43
|
+
if (member !== null) return { node: member, rest: inner.rest };
|
|
44
|
+
const parsed = parseNode(inner.body, spelling);
|
|
45
|
+
if (parsed !== null && parsed.rest.trim().length === 0)
|
|
46
|
+
return { node: parsed.node, rest: inner.rest };
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// a CALL first: a member read and a call both start with an
|
|
50
|
+
// identifier, and only the declared names are calls — trying the
|
|
51
|
+
// member first would swallow the call's own arguments
|
|
52
|
+
const match = /^([A-Za-z_][A-Za-z0-9_.]*)\s*\(/.exec(source);
|
|
53
|
+
const name = match === null ? null : spelling.nameOf(match[1]);
|
|
54
|
+
if (match !== null && name !== null) {
|
|
55
|
+
source = source.slice(match[1].length).trimStart();
|
|
56
|
+
const inner = balanced(source);
|
|
57
|
+
if (inner === null) return null;
|
|
58
|
+
const args = [];
|
|
59
|
+
let body = inner.body.trim();
|
|
60
|
+
while (body.length > 0) {
|
|
61
|
+
const parsed = parseNode(body, spelling);
|
|
62
|
+
if (parsed === null) return null;
|
|
63
|
+
args.push(parsed.node);
|
|
64
|
+
body = parsed.rest.trimStart();
|
|
65
|
+
if (body.startsWith(',')) { body = body.slice(1); continue; }
|
|
66
|
+
if (body.length > 0) return null;
|
|
67
|
+
}
|
|
68
|
+
return { node: { call: name, args }, rest: inner.rest };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const member = memberPrefix(source, spelling);
|
|
72
|
+
if (member !== null) return member;
|
|
73
|
+
return literalPrefix(source, spelling);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A member read at the head of `text`, as this dialect spells one.
|
|
78
|
+
* @param {string} text
|
|
79
|
+
* @param {any} spelling
|
|
80
|
+
* @returns {{ node: any, rest: string } | null}
|
|
81
|
+
*/
|
|
82
|
+
function memberPrefix(text, spelling) {
|
|
83
|
+
// the whole remaining head up to a comma or a closing parenthesis at
|
|
84
|
+
// depth zero: a member read is a self-contained expression
|
|
85
|
+
const head = headOf(text);
|
|
86
|
+
if (head === null) return null;
|
|
87
|
+
const member = spelling.memberOf(head.body);
|
|
88
|
+
return member === null ? null : { node: member, rest: head.rest };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A literal at the head of `text`.
|
|
93
|
+
* @param {string} text
|
|
94
|
+
* @param {any} spelling
|
|
95
|
+
* @returns {{ node: any, rest: string } | null}
|
|
96
|
+
*/
|
|
97
|
+
function literalPrefix(text, spelling) {
|
|
98
|
+
const head = headOf(text);
|
|
99
|
+
if (head === null) return null;
|
|
100
|
+
const body = head.body.trim();
|
|
101
|
+
const string = spelling.stringOf(body);
|
|
102
|
+
if (string !== null) return { node: { value: string }, rest: head.rest };
|
|
103
|
+
if (/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(body))
|
|
104
|
+
return { node: { value: Number(body) }, rest: head.rest };
|
|
105
|
+
if (body === 'TRUE' || body === '1') return { node: { value: true }, rest: head.rest };
|
|
106
|
+
if (body === 'FALSE' || body === '0') return { node: { value: false }, rest: head.rest };
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The text up to the next comma or unmatched close parenthesis at depth
|
|
112
|
+
* zero, and what follows it.
|
|
113
|
+
* @param {string} text
|
|
114
|
+
* @returns {{ body: string, rest: string } | null}
|
|
115
|
+
*/
|
|
116
|
+
function headOf(text) {
|
|
117
|
+
let depth = 0;
|
|
118
|
+
for (let i = 0; i < text.length; i++) {
|
|
119
|
+
const c = text[i];
|
|
120
|
+
if (c === "'") {
|
|
121
|
+
i = text.indexOf("'", i + 1);
|
|
122
|
+
if (i < 0) return null;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (c === '(') depth += 1;
|
|
126
|
+
else if (c === ')') {
|
|
127
|
+
if (depth === 0) return { body: text.slice(0, i), rest: text.slice(i) };
|
|
128
|
+
depth -= 1;
|
|
129
|
+
}
|
|
130
|
+
else if (c === ',' && depth === 0) return { body: text.slice(0, i), rest: text.slice(i) };
|
|
131
|
+
}
|
|
132
|
+
return { body: text, rest: '' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The body of the parenthesised group `text` opens with, and what
|
|
137
|
+
* follows the close.
|
|
138
|
+
* @param {string} text
|
|
139
|
+
* @returns {{ body: string, rest: string } | null}
|
|
140
|
+
*/
|
|
141
|
+
function balanced(text) {
|
|
142
|
+
if (!text.startsWith('(')) return null;
|
|
143
|
+
let depth = 0;
|
|
144
|
+
for (let i = 0; i < text.length; i++) {
|
|
145
|
+
const c = text[i];
|
|
146
|
+
if (c === "'") {
|
|
147
|
+
i = text.indexOf("'", i + 1);
|
|
148
|
+
if (i < 0) return null;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (c === '(') depth += 1;
|
|
152
|
+
else if (c === ')') {
|
|
153
|
+
depth -= 1;
|
|
154
|
+
if (depth === 0) return { body: text.slice(1, i), rest: text.slice(i + 1) };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|