@jarenjs/db 0.34.2 → 0.43.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 +115 -12
- package/README.md +47 -0
- package/dist/types/algebra.d.ts +38 -2
- package/dist/types/ddl.d.ts +40 -6
- package/dist/types/derive.d.ts +161 -0
- package/dist/types/dialect.d.ts +11 -2
- package/dist/types/driver.d.ts +2 -20
- package/dist/types/emit.d.ts +6 -3
- package/dist/types/errors.d.ts +6 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/migrate.d.ts +7 -1
- package/dist/types/plan.d.ts +15 -1
- package/dist/types/residual.d.ts +17 -6
- package/dist/types/udf.d.ts +6 -1
- package/docs/MIGRATION-FORMAT.md +30 -1
- package/docs/MODEL-FORMAT.md +155 -4
- package/package.json +4 -4
- package/schemas/jaren-migration.draft-07.schema.json +71 -0
- package/schemas/jaren-migration.schema.json +71 -0
- package/schemas/jaren-model.draft-07.schema.json +14 -1
- package/schemas/jaren-model.schema.json +18 -5
- package/src/algebra.js +17 -2
- package/src/ddl.js +146 -17
- package/src/derive.js +284 -0
- package/src/dialect.js +89 -25
- package/src/dialects/sqlite.js +16 -1
- package/src/driver.js +6 -28
- package/src/emit.js +122 -22
- package/src/errors.js +6 -4
- package/src/index.js +5 -0
- package/src/migrate.js +132 -19
- package/src/plan.js +514 -32
- package/src/query.js +61 -9
- package/src/residual.js +18 -10
- package/src/store.js +122 -8
- package/src/udf.js +12 -3
package/src/dialect.js
CHANGED
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
* limitClause: (limit: number, offset?: number) => string,
|
|
43
43
|
* jsonPathText: (segments: JsonPathSegment[]) => string | null,
|
|
44
44
|
* jsonExtract: (columnSql: string, pathText: string) => string,
|
|
45
|
+
* derivedExpression?: (memberSql: string, column: { derive: string,
|
|
46
|
+
* precision?: number, component?: string }) => string,
|
|
45
47
|
* jsonSet: (exprSql: string, pathText: string, valueSql: string) => string,
|
|
46
48
|
* jsonRemove: (exprSql: string, pathText: string) => string,
|
|
47
49
|
* jsonAppend: (exprSql: string, arrayPathText: string, valueSql: string) => string,
|
|
@@ -50,7 +52,8 @@
|
|
|
50
52
|
* jsonAgg: (exprSql: string) => string,
|
|
51
53
|
* jsonTypeOf: (columnSql: string, pathText: string) => string,
|
|
52
54
|
* valueTypeOf: (paramSql: string) => string,
|
|
53
|
-
* strStartsWith: (valueSql: string,
|
|
55
|
+
* strStartsWith: (valueSql: string, lowerParamSql: string, upperParamSql: string) => string,
|
|
56
|
+
* strStartsWithExact: (valueSql: string, patternA: string, patternB: string) => string,
|
|
54
57
|
* strEndsWith: (valueSql: string, patternA: string, patternB: string, patternC: string) => string,
|
|
55
58
|
* strContains: (valueSql: string, patternSql: string) => string,
|
|
56
59
|
* orderNulls: (nullsFirst: boolean) => string,
|
|
@@ -77,6 +80,24 @@ export function createDialect(spec) {
|
|
|
77
80
|
const q = spec.quoteIdentifier;
|
|
78
81
|
const p = spec.parameterRef;
|
|
79
82
|
|
|
83
|
+
/**
|
|
84
|
+
* One planned column's definition. A path column is a VIRTUAL
|
|
85
|
+
* generated column over the document; a DERIVED column is the same
|
|
86
|
+
* shape over a registered deterministic function, except where the
|
|
87
|
+
* driver cannot index one — there the store writes the value and the
|
|
88
|
+
* column is an ordinary one.
|
|
89
|
+
* @param {string} docColumn
|
|
90
|
+
* @param {{ name: string, type: string, pathText: string,
|
|
91
|
+
* expression?: string | null, stored?: boolean }} column
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
const generatedColumnSql = (docColumn, column) => {
|
|
95
|
+
if (column.stored === true) return `${q(column.name)} ${column.type}`;
|
|
96
|
+
const expression = column.expression
|
|
97
|
+
?? spec.jsonExtract(q(docColumn), column.pathText);
|
|
98
|
+
return `${q(column.name)} ${column.type} GENERATED ALWAYS AS (${expression}) VIRTUAL`;
|
|
99
|
+
};
|
|
100
|
+
|
|
80
101
|
const ddl = Object.freeze({
|
|
81
102
|
/**
|
|
82
103
|
* One collection's physical table: a key column, the JSON document
|
|
@@ -90,9 +111,7 @@ export function createDialect(spec) {
|
|
|
90
111
|
const columns = [
|
|
91
112
|
`${q(keyColumn)} ${keyType} PRIMARY KEY`,
|
|
92
113
|
`${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`),
|
|
114
|
+
...generated.map((g) => generatedColumnSql(docColumn, g)),
|
|
96
115
|
];
|
|
97
116
|
return `CREATE TABLE ${q(table)} (${columns.join(', ')})${spec.tableSuffix}`;
|
|
98
117
|
},
|
|
@@ -113,8 +132,7 @@ export function createDialect(spec) {
|
|
|
113
132
|
* @returns {string}
|
|
114
133
|
*/
|
|
115
134
|
addGeneratedColumn({ table, docColumn, column }) {
|
|
116
|
-
return `ALTER TABLE ${q(table)} ADD COLUMN ${
|
|
117
|
-
+ `GENERATED ALWAYS AS (${spec.jsonExtract(q(docColumn), column.pathText)}) VIRTUAL`;
|
|
135
|
+
return `ALTER TABLE ${q(table)} ADD COLUMN ${generatedColumnSql(docColumn, column)}`;
|
|
118
136
|
},
|
|
119
137
|
/**
|
|
120
138
|
* @param {string} table
|
|
@@ -217,26 +235,47 @@ export function createDialect(spec) {
|
|
|
217
235
|
});
|
|
218
236
|
|
|
219
237
|
const dml = Object.freeze({
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
/**
|
|
239
|
+
* A collection insert. `stored` names the derived columns the
|
|
240
|
+
* store computes itself — empty on every driver that can index a
|
|
241
|
+
* registered function, because there the column generates itself.
|
|
242
|
+
* @param {{ table: string, keyColumn: string, docColumn: string,
|
|
243
|
+
* stored?: string[] }} s
|
|
244
|
+
*/
|
|
245
|
+
insert({ table, keyColumn, docColumn, stored }) {
|
|
246
|
+
const extra = stored ?? [];
|
|
247
|
+
const names = [q(keyColumn), q(docColumn), ...extra.map(q)];
|
|
248
|
+
const values = [p(1, 'key'), spec.jsonEncode(p(2, 'doc')),
|
|
249
|
+
...extra.map((name, i) => p(i + 3, name))];
|
|
250
|
+
return `INSERT INTO ${q(table)} (${names.join(', ')}) VALUES (${values.join(', ')})`;
|
|
224
251
|
},
|
|
225
252
|
/**
|
|
226
253
|
* Insert with a database-allocated key, read back in the same
|
|
227
254
|
* statement.
|
|
228
|
-
* @param {{ table: string, keyColumn: string, docColumn: string
|
|
255
|
+
* @param {{ table: string, keyColumn: string, docColumn: string,
|
|
256
|
+
* stored?: string[] }} s
|
|
229
257
|
*/
|
|
230
|
-
insertAllocated({ table, keyColumn, docColumn }) {
|
|
231
|
-
|
|
232
|
-
|
|
258
|
+
insertAllocated({ table, keyColumn, docColumn, stored }) {
|
|
259
|
+
const extra = stored ?? [];
|
|
260
|
+
const names = [q(docColumn), ...extra.map(q)];
|
|
261
|
+
const values = [spec.jsonEncode(p(1, 'doc')),
|
|
262
|
+
...extra.map((name, i) => p(i + 2, name))];
|
|
263
|
+
return `INSERT INTO ${q(table)} (${names.join(', ')}) VALUES (${values.join(', ')}) `
|
|
264
|
+
+ `RETURNING ${q(keyColumn)} AS ${q('key')}`;
|
|
233
265
|
},
|
|
234
|
-
/**
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
/**
|
|
267
|
+
* @param {{ table: string, keyColumn: string, docColumn: string,
|
|
268
|
+
* stored?: string[] }} s
|
|
269
|
+
*/
|
|
270
|
+
upsert({ table, keyColumn, docColumn, stored }) {
|
|
271
|
+
const extra = stored ?? [];
|
|
272
|
+
const names = [q(keyColumn), q(docColumn), ...extra.map(q)];
|
|
273
|
+
const values = [p(1, 'key'), spec.jsonEncode(p(2, 'doc')),
|
|
274
|
+
...extra.map((name, i) => p(i + 3, name))];
|
|
275
|
+
const assignments = [q(docColumn), ...extra.map(q)]
|
|
276
|
+
.map((column) => `${column} = ${spec.excludedRef(column)}`);
|
|
277
|
+
return `INSERT INTO ${q(table)} (${names.join(', ')}) VALUES (${values.join(', ')}) `
|
|
278
|
+
+ `ON CONFLICT (${q(keyColumn)}) DO UPDATE SET ${assignments.join(', ')}`;
|
|
240
279
|
},
|
|
241
280
|
/** @param {{ table: string, keyColumn: string, docColumn: string }} s */
|
|
242
281
|
get({ table, keyColumn, docColumn }) {
|
|
@@ -250,13 +289,26 @@ export function createDialect(spec) {
|
|
|
250
289
|
/**
|
|
251
290
|
* Rewrite the document column through a JSON-set expression chain
|
|
252
291
|
* (the translated-patch path) or a bound parameter (the fallback).
|
|
253
|
-
*
|
|
292
|
+
* Stored derived columns are rewritten with it — they are computed
|
|
293
|
+
* FROM the document, so leaving them behind would let a query read a
|
|
294
|
+
* value the document no longer carries.
|
|
295
|
+
*
|
|
296
|
+
* `nextParam` is the first FREE parameter slot after the
|
|
297
|
+
* expression's own: the derived values take it and the ones after
|
|
298
|
+
* it, and the key binds LAST. That order is the statement's TEXT
|
|
299
|
+
* order, which is what a positional dialect numbers by — and with
|
|
300
|
+
* no derived columns it degenerates to the key at `nextParam`.
|
|
301
|
+
* @param {{ table: string, keyColumn: string, docColumn: string,
|
|
302
|
+
* stored?: string[] }} s
|
|
254
303
|
* @param {string} expression - SQL over the document column
|
|
255
|
-
* @param {number}
|
|
304
|
+
* @param {number} nextParam - 1-based first free parameter slot
|
|
256
305
|
*/
|
|
257
|
-
updateDoc({ table, keyColumn, docColumn }, expression,
|
|
258
|
-
|
|
259
|
-
|
|
306
|
+
updateDoc({ table, keyColumn, docColumn, stored }, expression, nextParam) {
|
|
307
|
+
const extra = stored ?? [];
|
|
308
|
+
const assignments = [`${q(docColumn)} = ${expression}`,
|
|
309
|
+
...extra.map((name, i) => `${q(name)} = ${p(nextParam + i, name)}`)];
|
|
310
|
+
return `UPDATE ${q(table)} SET ${assignments.join(', ')} `
|
|
311
|
+
+ `WHERE ${q(keyColumn)} = ${p(nextParam + extra.length, 'key')}`;
|
|
260
312
|
},
|
|
261
313
|
});
|
|
262
314
|
|
|
@@ -272,6 +324,17 @@ export function createDialect(spec) {
|
|
|
272
324
|
limitClause: spec.limitClause,
|
|
273
325
|
jsonPathText: spec.jsonPathText,
|
|
274
326
|
jsonExtract: spec.jsonExtract,
|
|
327
|
+
/**
|
|
328
|
+
* The expression a DERIVED column is generated from: the member at
|
|
329
|
+
* the index path, as JSON text, handed to the deterministic
|
|
330
|
+
* function that computes the cell or the box edge.
|
|
331
|
+
* @param {string} docColumnSql
|
|
332
|
+
* @param {string} pathText
|
|
333
|
+
* @param {{ derive: string, precision?: number, component?: string }} column
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
derivedColumn: (docColumnSql, pathText, column) => spec.derivedExpression(
|
|
337
|
+
spec.jsonText(spec.jsonExtract(docColumnSql, pathText)), column),
|
|
275
338
|
jsonSet: spec.jsonSet,
|
|
276
339
|
jsonRemove: spec.jsonRemove,
|
|
277
340
|
jsonAppend: spec.jsonAppend,
|
|
@@ -281,6 +344,7 @@ export function createDialect(spec) {
|
|
|
281
344
|
jsonTypeOf: spec.jsonTypeOf,
|
|
282
345
|
valueTypeOf: spec.valueTypeOf,
|
|
283
346
|
strStartsWith: spec.strStartsWith,
|
|
347
|
+
strStartsWithExact: spec.strStartsWithExact,
|
|
284
348
|
strEndsWith: spec.strEndsWith,
|
|
285
349
|
strContains: spec.strContains,
|
|
286
350
|
orderNulls: spec.orderNulls,
|
package/src/dialects/sqlite.js
CHANGED
|
@@ -101,6 +101,14 @@ export const sqliteDialect = createDialect({
|
|
|
101
101
|
jsonPathText,
|
|
102
102
|
jsonExtract: (columnSql, pathText) =>
|
|
103
103
|
`jsonb_extract(${columnSql}, ${stringLiteral(pathText)})`,
|
|
104
|
+
// a DERIVED column's expression: the member as JSON text handed to
|
|
105
|
+
// the deterministic function the store registers at open. The
|
|
106
|
+
// precision is a LITERAL, not a parameter — a generated column's
|
|
107
|
+
// expression takes none — which is also what makes two precisions
|
|
108
|
+
// over one path two different columns by declared text.
|
|
109
|
+
derivedExpression: (memberSql, column) => (column.derive === 'geohash'
|
|
110
|
+
? `jaren_geohash(${memberSql}, ${Math.trunc(Number(column.precision))})`
|
|
111
|
+
: `jaren_bbox_${column.component}(${memberSql})`),
|
|
104
112
|
jsonSet: (exprSql, pathText, valueSql) =>
|
|
105
113
|
`jsonb_set(${exprSql}, ${stringLiteral(pathText)}, ${valueSql})`,
|
|
106
114
|
jsonRemove: (exprSql, pathText) =>
|
|
@@ -113,7 +121,14 @@ export const sqliteDialect = createDialect({
|
|
|
113
121
|
jsonTypeOf: (columnSql, pathText) =>
|
|
114
122
|
`json_type(${columnSql}, ${stringLiteral(pathText)})`,
|
|
115
123
|
valueTypeOf: (paramSql) => `typeof(${paramSql})`,
|
|
116
|
-
|
|
124
|
+
// the half-open range over the prefix, which an index on the value
|
|
125
|
+
// can seek; `substr(value, 1, n) = p` and `value LIKE 'p%'` both read
|
|
126
|
+
// every row. SQLite's default BINARY collation compares UTF-8 bytes,
|
|
127
|
+
// which orders code points, so the range holds exactly the values
|
|
128
|
+
// that begin with the prefix
|
|
129
|
+
strStartsWith: (valueSql, lowerParamSql, upperParamSql) =>
|
|
130
|
+
`(${valueSql} >= ${lowerParamSql} AND ${valueSql} < ${upperParamSql})`,
|
|
131
|
+
strStartsWithExact: (valueSql, patternA, patternB) =>
|
|
117
132
|
`substr(${valueSql}, 1, length(${patternA})) = ${patternB}`,
|
|
118
133
|
strEndsWith: (valueSql, patternA, patternB, patternC) =>
|
|
119
134
|
`(length(${patternA}) = 0 OR substr(${valueSql}, -length(${patternB})) = ${patternC})`,
|
package/src/driver.js
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
* silent degradation this suite refuses.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
import { isThenable, chain, toPromise } from '@jarenjs/core/function';
|
|
34
|
+
|
|
33
35
|
import { DbCompileError } from './errors.js';
|
|
34
36
|
|
|
35
37
|
/** The minimum SQLite the store accepts, asserted at open. */
|
|
@@ -44,34 +46,10 @@ export const SQLITE_FLOOR = '3.45.0';
|
|
|
44
46
|
*/
|
|
45
47
|
export const DEFAULT_QUEUE_TIMEOUT = 5000;
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export function isThenable(value) {
|
|
52
|
-
return value !== null && typeof value === 'object' && typeof value.then === 'function';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Sync-capable-async composition: apply `next` to a driver result
|
|
57
|
-
* without allocating a promise when the result is already a value.
|
|
58
|
-
* @param {any} value - A driver return: a value or a promise
|
|
59
|
-
* @param {(value: any) => any} next
|
|
60
|
-
* @returns {any} `next`'s result, promise-wrapped only if the input was
|
|
61
|
-
*/
|
|
62
|
-
export function chain(value, next) {
|
|
63
|
-
return isThenable(value) ? value.then(next) : next(value);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Lift a driver result into a promise — the ONE allocation the public
|
|
68
|
-
* asynchronous surface pays per call.
|
|
69
|
-
* @param {any} value
|
|
70
|
-
* @returns {Promise<any>}
|
|
71
|
-
*/
|
|
72
|
-
export function toPromise(value) {
|
|
73
|
-
return isThenable(value) ? value : Promise.resolve(value);
|
|
74
|
-
}
|
|
49
|
+
// the sync-capable-async helpers are `@jarenjs/core/function`'s (one
|
|
50
|
+
// implementation in the suite); re-exported here because every store
|
|
51
|
+
// module and the public `@jarenjs/db` surface reach them through this seam
|
|
52
|
+
export { isThenable, chain, toPromise };
|
|
75
53
|
|
|
76
54
|
/**
|
|
77
55
|
* Compare two dotted version strings numerically.
|
package/src/emit.js
CHANGED
|
@@ -16,10 +16,84 @@
|
|
|
16
16
|
* logic never decides a row.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { codePointPrefixSuccessor } from '@jarenjs/core/string';
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
|
-
* @typedef {{ external: string } | { literal: unknown }
|
|
22
|
+
* @typedef {{ external: string } | { literal: unknown } |
|
|
23
|
+
* { derived: { kind: 'bboxAxis', external: string,
|
|
24
|
+
* axis: 'w' | 's' | 'e' | 'n' } }} ParamSlot
|
|
25
|
+
* Three kinds, closed. A DERIVED slot is the escape for a value SQL
|
|
26
|
+
* cannot bind at all: a GeoJSON region arrives as an external object,
|
|
27
|
+
* and what the statement needs is one edge of its bounding box, so
|
|
28
|
+
* the binder computes that edge from the bound value. It is the same
|
|
29
|
+
* shape the prefix successor uses — compute in JavaScript what SQL
|
|
30
|
+
* cannot, bind an ordinary parameter — and it stays closed on
|
|
31
|
+
* purpose: a general expression slot would be a second query language
|
|
32
|
+
* living in the emitter.
|
|
21
33
|
*/
|
|
22
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The SQL for one string predicate. The prefix case is the only one
|
|
37
|
+
* with an index-usable spelling: a function of the value (`substr`) or
|
|
38
|
+
* a pattern match (`LIKE`) can only be scanned, while the half-open
|
|
39
|
+
* range `value >= p AND value < successor(p)` is a seek, and holds
|
|
40
|
+
* exactly the values beginning with `p` under a code-point ordering.
|
|
41
|
+
* The planner reaches here only with a non-empty literal pattern, so
|
|
42
|
+
* the successor is computable at emit time and needs no slot kind of
|
|
43
|
+
* its own; the one pattern that has no successor keeps the scannable
|
|
44
|
+
* form, which is correct, merely slow.
|
|
45
|
+
* @param {any} dialect
|
|
46
|
+
* @param {(slot: ParamSlot) => string} param
|
|
47
|
+
* @param {string} valueSql
|
|
48
|
+
* @param {any} pred
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function stropForm(dialect, param, valueSql, pred) {
|
|
52
|
+
const pattern = pred.operand.lit;
|
|
53
|
+
const bind = () => param({ literal: pattern });
|
|
54
|
+
if (pred.kind === 'ends')
|
|
55
|
+
return dialect.strEndsWith(valueSql, bind(), bind(), bind());
|
|
56
|
+
if (pred.kind === 'contains')
|
|
57
|
+
return dialect.strContains(valueSql, bind());
|
|
58
|
+
const upper = codePointPrefixSuccessor(pattern);
|
|
59
|
+
return upper === null
|
|
60
|
+
? dialect.strStartsWithExact(valueSql, bind(), bind())
|
|
61
|
+
: dialect.strStartsWith(valueSql, bind(), param({ literal: upper }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The name a dialect uses for one slot's parameter reference.
|
|
66
|
+
* @param {ParamSlot} slot
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function slotName(slot) {
|
|
70
|
+
if ('external' in slot) return slot.external;
|
|
71
|
+
if ('derived' in slot) return slot.derived.external;
|
|
72
|
+
return 'value';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Where each edge sits in a `[west, south, east, north]` box. */
|
|
76
|
+
const BOX_AT = { w: 0, s: 1, e: 2, n: 3 };
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Bind one edge of the probe box: a plan-time literal box binds its own
|
|
80
|
+
* number, an external one binds a derived slot the binder computes from
|
|
81
|
+
* the value at call time.
|
|
82
|
+
*
|
|
83
|
+
* A positional dialect numbers parameters by the statement's TEXT
|
|
84
|
+
* order, so this is called in the order the placeholders appear and
|
|
85
|
+
* never in the order the box carries its edges.
|
|
86
|
+
* @param {any} probe
|
|
87
|
+
* @param {(slot: ParamSlot) => string} param
|
|
88
|
+
* @param {'w' | 's' | 'e' | 'n'} axis
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
function probeEdge(probe, param, axis) {
|
|
92
|
+
return 'box' in probe
|
|
93
|
+
? param({ literal: probe.box[BOX_AT[axis]] })
|
|
94
|
+
: param({ derived: { kind: 'bboxAxis', external: probe.ext, axis } });
|
|
95
|
+
}
|
|
96
|
+
|
|
23
97
|
/**
|
|
24
98
|
* Emit one plan as SQL plus its ordered parameter slots.
|
|
25
99
|
* @param {import('./algebra.js').Plan} plan
|
|
@@ -34,7 +108,7 @@ export function emitPlan(plan, dialect, physical) {
|
|
|
34
108
|
const slots = [];
|
|
35
109
|
const param = (slot) => {
|
|
36
110
|
slots.push(slot);
|
|
37
|
-
return dialect.parameterRef(slots.length,
|
|
111
|
+
return dialect.parameterRef(slots.length, slotName(slot));
|
|
38
112
|
};
|
|
39
113
|
|
|
40
114
|
/** SQL for a ref's VALUE: the generated column when one exists. */
|
|
@@ -132,15 +206,51 @@ export function emitPlan(plan, dialect, physical) {
|
|
|
132
206
|
return `${pred.name}(${dialect.jsonText(docColumn)})`;
|
|
133
207
|
case 'strop': {
|
|
134
208
|
const jt = typeOf(pred.ref);
|
|
135
|
-
const
|
|
136
|
-
const bind = () => param({ literal: /** @type {any} */ (pred.operand).lit });
|
|
137
|
-
const form = pred.kind === 'starts'
|
|
138
|
-
? dialect.strStartsWith(value, bind(), bind())
|
|
139
|
-
: pred.kind === 'ends'
|
|
140
|
-
? dialect.strEndsWith(value, bind(), bind(), bind())
|
|
141
|
-
: dialect.strContains(value, bind());
|
|
209
|
+
const form = stropForm(dialect, param, valueOf(pred.ref), pred);
|
|
142
210
|
return `(${jt} IS NOT NULL AND ${jt} = ${sl('text')} AND ${form})`;
|
|
143
211
|
}
|
|
212
|
+
case 'bboxOverlap': {
|
|
213
|
+
// Two boxes meet when neither is wholly past the other, and
|
|
214
|
+
// TOUCHING counts (`bboxIntersects`), so these are `<=`/`>=`:
|
|
215
|
+
// a strict comparison would disagree with the engine on every
|
|
216
|
+
// shared edge. Emitted in the index's covered column order —
|
|
217
|
+
// (w, e, s, n) — so the leading longitude bound sits in front.
|
|
218
|
+
// The leading `IS NOT NULL` keeps the form TOTAL — a row with no
|
|
219
|
+
// box answers FALSE, not NULL, so a negation over an exact box
|
|
220
|
+
// test still composes classically — and it is also what makes
|
|
221
|
+
// the term SEEKABLE: it bounds the leading column from below,
|
|
222
|
+
// and a one-sided range alone loses to a table scan in SQLite's
|
|
223
|
+
// cost model, which prices a virtual generated column as a free
|
|
224
|
+
// column read when it is a host function call per row.
|
|
225
|
+
const c = pred.columns;
|
|
226
|
+
const edge = (axis) => probeEdge(pred.probe, param, axis);
|
|
227
|
+
return `(${q(c.w)} IS NOT NULL AND ${q(c.w)} <= ${edge('e')}`
|
|
228
|
+
+ ` AND ${q(c.e)} >= ${edge('w')}`
|
|
229
|
+
+ ` AND ${q(c.s)} <= ${edge('n')} AND ${q(c.n)} >= ${edge('s')})`;
|
|
230
|
+
}
|
|
231
|
+
case 'cellIn': {
|
|
232
|
+
// The cells are whole values of the column, so this is an
|
|
233
|
+
// equality set — and `IN` is the spelling that keeps it one:
|
|
234
|
+
// nine OR-ed ranges defeat SQLite's multi-index OR optimization
|
|
235
|
+
// (it gives up past a handful of terms) and fall back to a scan,
|
|
236
|
+
// which for a virtual generated column is a host function call
|
|
237
|
+
// per row.
|
|
238
|
+
const column = q(pred.column);
|
|
239
|
+
const list = pred.cells.map((cell) => param({ literal: cell })).join(', ');
|
|
240
|
+
return `(${column} IS NOT NULL AND ${column} IN (${list}))`;
|
|
241
|
+
}
|
|
242
|
+
case 'cellPrefix': {
|
|
243
|
+
// A cell SHORTER than the column's own: the half-open range an
|
|
244
|
+
// index seeks. Every cell is base-32, so the code-point
|
|
245
|
+
// successor always exists.
|
|
246
|
+
const column = q(pred.column);
|
|
247
|
+
const upper = codePointPrefixSuccessor(pred.prefix);
|
|
248
|
+
if (upper === null)
|
|
249
|
+
throw new Error('emit: a geohash cell has no code-point successor');
|
|
250
|
+
const range = dialect.strStartsWith(column, param({ literal: pred.prefix }),
|
|
251
|
+
param({ literal: upper }));
|
|
252
|
+
return `(${column} IS NOT NULL AND ${range})`;
|
|
253
|
+
}
|
|
144
254
|
default:
|
|
145
255
|
throw new Error(`emit: unknown predicate node '${/** @type {any} */ (pred).p}'`);
|
|
146
256
|
}
|
|
@@ -209,12 +319,7 @@ export function createEntityPredicateEmitters(dialect, param) {
|
|
|
209
319
|
: `(${jt} IS NOT NULL AND ${jt} NOT IN (${list}))`;
|
|
210
320
|
}
|
|
211
321
|
if (pred.p === 'strop') {
|
|
212
|
-
const
|
|
213
|
-
const form = pred.kind === 'starts'
|
|
214
|
-
? dialect.strStartsWith(value, bind(), bind())
|
|
215
|
-
: pred.kind === 'ends'
|
|
216
|
-
? dialect.strEndsWith(value, bind(), bind(), bind())
|
|
217
|
-
: dialect.strContains(value, bind());
|
|
322
|
+
const form = stropForm(dialect, param, value, pred);
|
|
218
323
|
return `(${jt} IS NOT NULL AND ${jt} = ${sl('text')} AND ${form})`;
|
|
219
324
|
}
|
|
220
325
|
const lit = pred.operand.lit;
|
|
@@ -243,12 +348,7 @@ export function createEntityPredicateEmitters(dialect, param) {
|
|
|
243
348
|
: `(${column} IS NOT NULL AND ${column} <> ${param({ literal: wanted })})`;
|
|
244
349
|
}
|
|
245
350
|
if (pred.p === 'strop') {
|
|
246
|
-
const
|
|
247
|
-
const form = pred.kind === 'starts'
|
|
248
|
-
? dialect.strStartsWith(column, bind(), bind())
|
|
249
|
-
: pred.kind === 'ends'
|
|
250
|
-
? dialect.strEndsWith(column, bind(), bind(), bind())
|
|
251
|
-
: dialect.strContains(column, bind());
|
|
351
|
+
const form = stropForm(dialect, param, column, pred);
|
|
252
352
|
return `(${column} IS NOT NULL AND ${form})`;
|
|
253
353
|
}
|
|
254
354
|
const symbol = { eq: '=', ne: '<>', lt: '<', le: '<=', gt: '>', ge: '>=' }[pred.op];
|
|
@@ -330,7 +430,7 @@ export function emitEntityPlan(plan, dialect, physicalOf) {
|
|
|
330
430
|
const slots = [];
|
|
331
431
|
const param = (slot) => {
|
|
332
432
|
slots.push(slot);
|
|
333
|
-
return dialect.parameterRef(slots.length,
|
|
433
|
+
return dialect.parameterRef(slots.length, slotName(slot));
|
|
334
434
|
};
|
|
335
435
|
|
|
336
436
|
const aliases = new Map(plan.bindings.map((binding, i) => [
|
package/src/errors.js
CHANGED
|
@@ -22,7 +22,7 @@ export const DB_CODES = Object.freeze({
|
|
|
22
22
|
JD0001: 'the SQLite library is below the supported floor',
|
|
23
23
|
JD0002: 'the declared model disagrees with the existing database',
|
|
24
24
|
JD0003: 'the driver binding is unavailable on this runtime',
|
|
25
|
-
JD0004: '
|
|
25
|
+
JD0004: 'a declared index cannot be mapped to a column',
|
|
26
26
|
JD0005: 'the model document is invalid',
|
|
27
27
|
JD0010: 'strict mode refused a residual',
|
|
28
28
|
JD0011: 'the profile refused the document',
|
|
@@ -66,9 +66,11 @@ export const DB_CODES = Object.freeze({
|
|
|
66
66
|
* - `JD0003` — the runtime builtin behind a driver could not be
|
|
67
67
|
* loaded here (Node cannot resolve `bun:`; Bun ships no
|
|
68
68
|
* `node:sqlite`), or an injected handle is missing
|
|
69
|
-
* - `JD0004` —
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* - `JD0004` — a declared index cannot be mapped to a column: its
|
|
70
|
+
* path does not select exactly one member (wildcards, slices,
|
|
71
|
+
* filters and descendants are not indexable), or its `derive`
|
|
72
|
+
* declaration is not one the storage vocabulary carries; the reason
|
|
73
|
+
* names the expression or the member and `docPath` points at it
|
|
72
74
|
* - `JD0005` — the model document is invalid; `docPath` points at
|
|
73
75
|
* the offending member
|
|
74
76
|
* - `JD0010` — `strict: true` and part of the query would have run
|
package/src/index.js
CHANGED
|
@@ -29,6 +29,11 @@ export { selectPlan, conjoin, assertNoSqlText, PLAN_VERSION } from './algebra.js
|
|
|
29
29
|
export { typeOfPath, isNumericType } from './types.js';
|
|
30
30
|
export { compileSetResidual, compileRowResidual, sequenceResult } from './residual.js';
|
|
31
31
|
export { deterministicFragment, registerFragment } from './udf.js';
|
|
32
|
+
export {
|
|
33
|
+
DERIVE_KINDS, BBOX_COMPONENTS, BBOX_INDEX_ORDER, PRECISION_MIN, PRECISION_MAX,
|
|
34
|
+
deriveGeohash, deriveBboxEdge, derivedValue, memberAt, storedMemberForm,
|
|
35
|
+
registerDeriveFunctions,
|
|
36
|
+
} from './derive.js';
|
|
32
37
|
export {
|
|
33
38
|
createQueryEngine, createQueryState, createEntityQueryEngine,
|
|
34
39
|
createLoadEngine, INCLUDE_DEPTH_DEFAULT,
|