@jarenjs/db 0.34.0 → 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/jobs.d.ts +7 -1
- 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/JOBS-FORMAT.md +12 -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/jobs.js +36 -7
- 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/ARCHITECTURE.md
CHANGED
|
@@ -66,6 +66,11 @@ binds arrays.
|
|
|
66
66
|
generated columns. The plan carries both the CREATE statements and
|
|
67
67
|
the structural facts an existing table must match (`JD0002` when it
|
|
68
68
|
does not — nothing is ever altered).
|
|
69
|
+
- `src/derive.js` — the one place a declared `derive` becomes a value.
|
|
70
|
+
A geohash cell or a bounding-box edge from `@jarenjs/core/geo`, and
|
|
71
|
+
the deterministic SQL functions a generated column's expression
|
|
72
|
+
calls. The SAME functions serve both physical mappings, so the two
|
|
73
|
+
branches cannot drift into different answers.
|
|
69
74
|
- `src/patch-sql.js` — RFC 6902 to JSON-set primitives, discriminated
|
|
70
75
|
against the live document (a pointer cannot say array-or-object on
|
|
71
76
|
its own). Untranslatable operations fall back to a whole-document
|
|
@@ -127,7 +132,9 @@ below, and that table's reasons are what `explain()` reports.
|
|
|
127
132
|
|
|
128
133
|
### The translated set
|
|
129
134
|
|
|
130
|
-
A single-binding FLWOR over the collection (`$for: {
|
|
135
|
+
A single-binding FLWOR over the collection (`$for: { <name>: '$[*]' }`
|
|
136
|
+
— the examples here write `it`, but the binding is the **document's** to
|
|
137
|
+
name and nothing translates differently under another one)
|
|
131
138
|
with: comparison predicates (`$eq $ne $lt $le $gt $ge`) between a
|
|
132
139
|
singular member path and a literal or external; `$and`/`$or`/`$not`
|
|
133
140
|
composition; `$exists`/`$empty`; `$starts-with`/`$ends-with`/
|
|
@@ -136,7 +143,15 @@ composition; `$exists`/`$empty`; `$starts-with`/`$ends-with`/
|
|
|
136
143
|
collation); a top-level `$subsequence` window with literal bounds; the
|
|
137
144
|
top-level aggregates `$count` (bare-binding return only) and
|
|
138
145
|
`$sum`/`$avg`/`$min`/`$max` over a singular schema-typed path; and the
|
|
139
|
-
whole-document projection
|
|
146
|
+
whole-document projection that returns the bare binding.
|
|
147
|
+
|
|
148
|
+
Plus the spatial predicates a **derived** index makes decidable:
|
|
149
|
+
`$bbox-intersects` against a literal or external region, a geohash
|
|
150
|
+
prefix no longer than a `derive: 'geohash'` column's precision, and the
|
|
151
|
+
nine-cell neighbourhood probe over that same column — each over a
|
|
152
|
+
member the schema types as an array or an object. Those are exact; the
|
|
153
|
+
spatial predicates that only NARROW are rows in the residual table, and
|
|
154
|
+
the implied-conjunct table below carries every proof.
|
|
140
155
|
|
|
141
156
|
### The deliberate-residual table
|
|
142
157
|
|
|
@@ -150,7 +165,11 @@ whole-document projection `$return: '$it'`.
|
|
|
150
165
|
| string operators with an external pattern | the pattern's type is unknowable at plan time and the engine ERRORS on non-string patterns |
|
|
151
166
|
| comparisons where both sides are paths | join territory |
|
|
152
167
|
| array/object literals in comparisons | deep-equality has no guarded native form |
|
|
153
|
-
|
|
|
168
|
+
| `$within` over a `derive: 'bbox'` column | a bounding-box pre-filter is pushed; exact containment refines in the engine |
|
|
169
|
+
| a bounded `$distance` over a `derive: 'bbox'` column | a geodesic-circle box pre-filter is pushed; the exact distance refines in the engine |
|
|
170
|
+
| a geohash prefix LONGER than the column's precision | a cell-range pre-filter over the derived column's precision is pushed; the longer prefix refines in the engine |
|
|
171
|
+
| a spatial predicate over a member with no matching derived index, or one the schema does not type as an array or an object | nothing is proven; the whole predicate runs in the engine (the deterministic-function hatch may still take it) |
|
|
172
|
+
| an unbounded `$distance` (`>= r`), a circle reaching a pole or crossing the antimeridian, a probe with no bounding box | no conservative box exists — pushing nothing is correct, pushing a wrong box is not |
|
|
154
173
|
|
|
155
174
|
### The type truth table
|
|
156
175
|
|
|
@@ -189,10 +208,20 @@ generated column where one exists), `?` = the bound operand:
|
|
|
189
208
|
| `$eq` path, external | `(jt = 'text' AND typeof(?) = 'text' AND v = ?) OR (jt IN ('integer','real') AND typeof(?) IN ('integer','real') AND v = ?)` |
|
|
190
209
|
| `$ne` path, external | `jt IS NOT NULL AND NOT (…the $eq form…)` |
|
|
191
210
|
| ordering vs external | the same two-branch form with `op` |
|
|
192
|
-
| `$starts-with` path, string | `jt = 'text' AND
|
|
211
|
+
| `$starts-with` path, string | `jt = 'text' AND (v >= ? AND v < ?)` — the prefix and its code-point successor |
|
|
193
212
|
| `$ends-with` path, string | `jt = 'text' AND (length(?) = 0 OR substr(v, -length(?)) = ?)` |
|
|
194
213
|
| `$contains` path, string | `jt = 'text' AND instr(v, ?) > 0` |
|
|
195
214
|
|
|
215
|
+
**The prefix predicate is index-usable, and the other two are not.**
|
|
216
|
+
`$starts-with` emits a half-open range over the value, so a declared
|
|
217
|
+
index on that path is *seeked*, not scanned — worth declaring one for.
|
|
218
|
+
The bounds are computed at emit time, which the planner's own rule
|
|
219
|
+
makes exact: a string operator translates only with a literal,
|
|
220
|
+
non-empty pattern, so its code-point successor is known there. (A
|
|
221
|
+
pattern of nothing but U+10FFFF has no successor and falls back to the
|
|
222
|
+
scannable `substr` form.) `$ends-with` and `$contains` have no
|
|
223
|
+
index-usable spelling and read every row of the collection.
|
|
224
|
+
|
|
196
225
|
The guards make the forms sound for typed AND untyped paths alike —
|
|
197
226
|
the schema type's job is choosing the generated COLUMN (the index),
|
|
198
227
|
never weakening the guard. Two documented preconditions: string
|
|
@@ -209,14 +238,78 @@ matters to you.
|
|
|
209
238
|
execute time, if any referenced external is missing or not a string or
|
|
210
239
|
finite number, the call runs the always-compiled set residual instead
|
|
211
240
|
of the native statement — same answer, one branch, no wrong-typed SQL.
|
|
241
|
+
A **derived** slot is the exception that proves it: a GeoJSON region is
|
|
242
|
+
not bindable at all, so what binds is one edge of its bounding box per
|
|
243
|
+
slot, computed at bind time; such a call diverts only when the bound
|
|
244
|
+
value has no box.
|
|
245
|
+
|
|
246
|
+
### The implied conjunct (spatial)
|
|
247
|
+
|
|
248
|
+
Every conjunct above is a conjunct **of the document**: it translates
|
|
249
|
+
exactly or it does not. A spatial predicate mostly cannot — there is no
|
|
250
|
+
`ST_Within` in SQLite and this package does not build one — but it
|
|
251
|
+
*implies* one that can be, over the columns a model declares with
|
|
252
|
+
`indexes[].derive` (MODEL-FORMAT §2.1).
|
|
253
|
+
|
|
254
|
+
> An **implied conjunct** is a predicate the planner ADDS to the SQL
|
|
255
|
+
> that is not in the document, proven below to be implied by one that
|
|
256
|
+
> is. It narrows; it never decides. The conjunct it came from stays in
|
|
257
|
+
> the residual.
|
|
258
|
+
|
|
259
|
+
Three properties make that safe, and each is asserted by test: **no
|
|
260
|
+
false negatives** (every row the document's predicate keeps passes the
|
|
261
|
+
implied one — the proofs below); **idempotent refinement** (the residual
|
|
262
|
+
re-runs the original predicate over the narrowed candidates, which is
|
|
263
|
+
why an implied conjunct forces the SET residual and not the row one);
|
|
264
|
+
and **`strict: true` refuses it** (an implied conjunct leaves its own
|
|
265
|
+
reason behind, so the plan is never native and `JD0010` names it).
|
|
266
|
+
|
|
267
|
+
Throughout, `B(v)` is the value's bounding box and `<c>_w`/`_s`/`_e`/`_n`
|
|
268
|
+
are a `bbox` index's columns; `<c>` is a `geohash` index's column at its
|
|
269
|
+
declared precision `k`. Every promotion requires the schema to type the
|
|
270
|
+
member as an array or an object **and nothing else** — §8.14 answers
|
|
271
|
+
`JQ2001` for a non-geographic operand, and a union that also admits
|
|
272
|
+
`null` is not geography.
|
|
273
|
+
|
|
274
|
+
| Jaren predicate | pushed | exact? | why it is implied |
|
|
275
|
+
|---|---|---|---|
|
|
276
|
+
| `$bbox-intersects(<path>, <literal\|external>)` | `w <= L_e AND e >= L_w AND s <= L_n AND n >= L_s` | **exact** | the derived columns ARE `B(row)`, so box overlap is fully decidable. `<=`/`>=`, not `<`/`>`: the kernel counts touching edges as intersecting, and a strict comparison would disagree on every shared edge |
|
|
277
|
+
| `$within(<path>, <literal\|external>)` | the same four comparisons against `B(area)` | implied | the representative position is inside `B(subject)` — a bare position IS the box, and a centroid is a mean of positions, which lies within their min/max — and inside the area's surface implies inside `B(area)`; so the two boxes share at least that position ∎ |
|
|
278
|
+
| `{$le\|$lt: [{$distance: [<path>, <literal>]}, r]}` | the same four comparisons against `circleBounds(probe, r)` | implied | every position within `r` metres lies inside the circle's box, which the kernel computes on the same sphere and the same `EARTH_RADIUS` the engine measures with — so the two cannot disagree by model. `$ge`/`$gt` is NOT promoted: no box narrows "farther than r" |
|
|
279
|
+
| `{$starts-with: [{$geohash: [<path>, k]}, "<cell>"]}`, cell length ≤ k | `<c> IN ("<cell>")` or `<c> >= "<cell>" AND <c> < successor` | **exact** | the column HOLDS `$geohash(row, k)`, and geohash is a prefix code, so a prefix test on the expression is the same test on the column |
|
|
280
|
+
| the same, cell length > k | the cell truncated to k | implied | the column can only confirm its own first k characters |
|
|
281
|
+
| `{$exists: {$index-of: [{$geohash-neighbours: "<cell>"}, {$geohash: [<path>, k]}]}}`, cell length = k | `<c> IN (…the nine cells…)` | **exact** | the membership test compares whole strings and the column is exactly one of them. Nine cells, never one: two points ten metres apart can differ in the FIRST character of their cell (D7), so a single prefix is bucketing and only the neighbourhood is proximity |
|
|
282
|
+
|
|
283
|
+
The implied forms carry no `json_type` guard — a derived column IS the
|
|
284
|
+
value — but each is TOTAL through its own `IS NOT NULL`, so a row with
|
|
285
|
+
no box answers `FALSE` rather than SQL's `NULL` and negation composes
|
|
286
|
+
classically. An implied conjunct may not be negated at all: negating a
|
|
287
|
+
superset is a subset, and that drops rows. That guard also makes the
|
|
288
|
+
leading term a two-sided range, which is what SQLite will actually
|
|
289
|
+
**seek**: with a one-sided range it prefers a table scan, and a scan over
|
|
290
|
+
a virtual generated column pays a registered-function call per row.
|
|
291
|
+
|
|
292
|
+
**A pre-filter decides which rows the engine SEES, so it also decides
|
|
293
|
+
which rows can raise.** The row set is unchanged — that is what the
|
|
294
|
+
table above proves — but a row the pre-filter excludes never reaches
|
|
295
|
+
the engine and therefore never throws: a stored `null` under a member
|
|
296
|
+
the schema types as geography, or a value with no bounded position
|
|
297
|
+
under §8.14's membership recipe (`$index-of` refuses an empty search
|
|
298
|
+
item). This is the same class as the string-operator and aggregate
|
|
299
|
+
preconditions above, and the same answer: keep `compileSchema` injected
|
|
300
|
+
if that distinction matters to you.
|
|
212
301
|
|
|
213
302
|
### The two residual modes
|
|
214
303
|
|
|
215
304
|
- **Row residual** — only the projection is untranslated: predicates,
|
|
216
305
|
ordering and the window are fully pushed; each fetched row runs
|
|
217
|
-
`{ $for: {
|
|
218
|
-
|
|
219
|
-
items concatenate in row
|
|
306
|
+
`{ $for: { <the document's own binding>: '$[*]' },
|
|
307
|
+
$return: [ <the document's $return> ] }` (the array wrapper keeps
|
|
308
|
+
array-valued items unambiguous) and the items concatenate in row
|
|
309
|
+
order. Streams. **The planner emits that one-row document whole**,
|
|
310
|
+
binding included, rather than handing the projection to be re-wrapped
|
|
311
|
+
elsewhere: the wrapper must bind what the projection references, and
|
|
312
|
+
only the planner knows what the caller called it.
|
|
220
313
|
- **Set residual** — anything else: the pushed predicate conjuncts
|
|
221
314
|
narrow candidates (`$and` splits; a partially translatable `$or`
|
|
222
315
|
does not), and the WHOLE original compiled document runs over the
|
|
@@ -227,13 +320,18 @@ of the native statement — same answer, one branch, no wrong-typed SQL.
|
|
|
227
320
|
|
|
228
321
|
Extends `compileJsonQuery(...).explain()`'s shape — `{ externals,
|
|
229
322
|
operators, functions, collations, limits }` — with `{ sql, params,
|
|
230
|
-
indexes, residual, barriers, scanNarrative }`. `params`
|
|
231
|
-
bound slots in order (external names
|
|
232
|
-
|
|
323
|
+
indexes, prefilters, residual, barriers, scanNarrative }`. `params`
|
|
324
|
+
lists the bound slots in order (external names, literal markers, and
|
|
325
|
+
derived slots naming the external and box axis they compute — values
|
|
326
|
+
are ALWAYS bound, never interpolated). `indexes` names the declared
|
|
233
327
|
indexes whose generated columns the pushed predicates and ordering
|
|
234
328
|
touch, and the `scanNarrative` is the database's own `EXPLAIN QUERY
|
|
235
329
|
PLAN` prose so the claim is checkable against the engine that will run
|
|
236
|
-
it. `
|
|
330
|
+
it. `prefilters` is the implied conjuncts — `{ construct, columns,
|
|
331
|
+
exact }` each — because whether a declared index is earning its keep is
|
|
332
|
+
not readable from `sql` alone, and because `indexes` says what a
|
|
333
|
+
predicate TOUCHES while the narrative says what the database will
|
|
334
|
+
DO. `estimatedRows` is ABSENT on SQLite drivers — the capability slot
|
|
237
335
|
is empty and no number is fabricated. `residual` is `null` or
|
|
238
336
|
`{ mode: 'row' | 'set', reasons: [{ construct, reason }] }` with
|
|
239
337
|
reasons drawn from the deliberate-residual table. With
|
|
@@ -247,7 +345,12 @@ predicate conjunct as a deterministic function used in the `WHERE`
|
|
|
247
345
|
clause. Gated on `capabilities.userFunctions` (absent on Bun by
|
|
248
346
|
construction) and applied only to fragments with no externals, no
|
|
249
347
|
functions and no collations — deterministic and side-effect-free by
|
|
250
|
-
analysis, not by hope.
|
|
348
|
+
analysis, not by hope. The fragment is a raw conjunct over the caller's
|
|
349
|
+
collection binding, so the planner passes that binding's **name** in:
|
|
350
|
+
wrapped under any other name every reference would read as an external,
|
|
351
|
+
the determinism rule would reject the fragment, and the hatch would
|
|
352
|
+
silently not engage — no error and no reason in `explain()`.
|
|
353
|
+
Registration is keyed by `contentKey(fragment)`
|
|
251
354
|
so identical fragments share one registration, and the planner MUST
|
|
252
355
|
produce a correct plan with the capability disabled (tested that way).
|
|
253
356
|
Preference order: native SQL → deterministic function → residual, and
|
package/README.md
CHANGED
|
@@ -86,6 +86,53 @@ const adults = await users.execute({
|
|
|
86
86
|
plus real indexes, typed from the collection's schema. Opening an
|
|
87
87
|
existing database verifies the declared shape and refuses to alter
|
|
88
88
|
it — reshaping is the migration story.
|
|
89
|
+
- **Spatial members get indexable columns.** A position is an array and
|
|
90
|
+
a geometry is an object, so neither is indexable as it stands. An
|
|
91
|
+
index declaring `derive: 'geohash'` (with a required `precision`) or
|
|
92
|
+
`derive: 'bbox'` materializes the cell, or the four box edges, as
|
|
93
|
+
columns computed by `@jarenjs/core/geo` — a generated column over a
|
|
94
|
+
registered deterministic function where the driver can index one, and
|
|
95
|
+
a stored column the store writes where it cannot.
|
|
96
|
+
- **A spatial query is two stages, and `explain()` names both.** A
|
|
97
|
+
`$within`, a `$bbox-intersects`, a bounded `$distance` or a geohash
|
|
98
|
+
probe over such a collection narrows **in SQLite** through the index
|
|
99
|
+
and refines **in the engine**. `$bbox-intersects` and a geohash cell
|
|
100
|
+
test are exact and need no refinement; `$within` and a bounded
|
|
101
|
+
`$distance` push a bounding box the truth table proves they imply,
|
|
102
|
+
and the exact predicate re-runs over the narrowed candidates —
|
|
103
|
+
`explain().prefilters` says which, over what columns, and whether it
|
|
104
|
+
decided or merely narrowed.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// the collection declares indexes: [{ name: 'by_box', path: '$.at',
|
|
108
|
+
// derive: 'bbox' }] and types that member at: { type: ['array', 'object'] }
|
|
109
|
+
// — a predicate is only PUSHED onto a member the schema types as geography
|
|
110
|
+
const places = store.collection('places');
|
|
111
|
+
const nearby = {
|
|
112
|
+
$for: { p: '$[*]' },
|
|
113
|
+
$where: { $within: ['$p.at', '$region'] },
|
|
114
|
+
$return: '$p',
|
|
115
|
+
};
|
|
116
|
+
await places.execute(nearby, { externals: { region } });
|
|
117
|
+
|
|
118
|
+
const how = await places.explain(nearby, { externals: { region } });
|
|
119
|
+
how.prefilters;
|
|
120
|
+
// [{ construct: '$within',
|
|
121
|
+
// columns: ['gx_at_bbox_w', 'gx_at_bbox_e', 'gx_at_bbox_s', 'gx_at_bbox_n'],
|
|
122
|
+
// exact: false }]
|
|
123
|
+
how.residual.reasons[0].reason;
|
|
124
|
+
// 'a bounding-box pre-filter is pushed; exact containment refines in the engine'
|
|
125
|
+
how.scanNarrative;
|
|
126
|
+
// 'SEARCH places USING INDEX places_by_box (gx_at_bbox_w>? AND gx_at_bbox_w<?); …'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The region arrives as a bound parameter: a GeoJSON object is not a
|
|
130
|
+
value any database can bind, so what binds is one edge of its box per
|
|
131
|
+
slot, computed at bind time from the same kernel the stored columns
|
|
132
|
+
came from. A circle that reaches a pole or crosses the antimeridian
|
|
133
|
+
pushes **nothing** — there is no single box to push — and the answer
|
|
134
|
+
is the same, reached by reading more rows. `explain()` is what makes
|
|
135
|
+
that checkable rather than quoted.
|
|
89
136
|
- **Migrations are documents.** `planMigration` diffs two models into
|
|
90
137
|
rendered-DDL + JSLT-transform + assertion steps; a shadow database
|
|
91
138
|
replays the whole chain before the real store is touched; a
|
package/dist/types/algebra.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* design (see ARCHITECTURE.md's deliberate-residual table).
|
|
14
14
|
*/
|
|
15
15
|
/** The plan format version, carried on every plan. */
|
|
16
|
-
export declare const PLAN_VERSION =
|
|
16
|
+
export declare const PLAN_VERSION = 2;
|
|
17
17
|
export type PlanRef = {
|
|
18
18
|
segments: ({
|
|
19
19
|
name: string;
|
|
@@ -56,6 +56,27 @@ export type PlanPredicate = ({
|
|
|
56
56
|
p: 'udf';
|
|
57
57
|
name: string;
|
|
58
58
|
key: string;
|
|
59
|
+
} | {
|
|
60
|
+
p: 'bboxOverlap';
|
|
61
|
+
columns: {
|
|
62
|
+
w: string;
|
|
63
|
+
s: string;
|
|
64
|
+
e: string;
|
|
65
|
+
n: string;
|
|
66
|
+
};
|
|
67
|
+
probe: {
|
|
68
|
+
box: number[];
|
|
69
|
+
} | {
|
|
70
|
+
ext: string;
|
|
71
|
+
};
|
|
72
|
+
} | {
|
|
73
|
+
p: 'cellIn';
|
|
74
|
+
column: string;
|
|
75
|
+
cells: string[];
|
|
76
|
+
} | {
|
|
77
|
+
p: 'cellPrefix';
|
|
78
|
+
column: string;
|
|
79
|
+
prefix: string;
|
|
59
80
|
});
|
|
60
81
|
export type PlanOrderTerm = {
|
|
61
82
|
ref: PlanRef;
|
|
@@ -96,8 +117,23 @@ export type Plan = {
|
|
|
96
117
|
* { p: 'strop', kind: 'starts' | 'ends' | 'contains',
|
|
97
118
|
* ref: PlanRef, operand: PlanOperand } |
|
|
98
119
|
* { p: 'const', value: boolean } |
|
|
99
|
-
* { p: 'udf', name: string, key: string }
|
|
120
|
+
* { p: 'udf', name: string, key: string } |
|
|
121
|
+
* { p: 'bboxOverlap', columns: { w: string, s: string, e: string,
|
|
122
|
+
* n: string }, probe: { box: number[] } | { ext: string } } |
|
|
123
|
+
* { p: 'cellIn', column: string, cells: string[] } |
|
|
124
|
+
* { p: 'cellPrefix', column: string, prefix: string }
|
|
100
125
|
* )} PlanPredicate
|
|
126
|
+
* The last three are the SPATIAL forms: predicates over the derived
|
|
127
|
+
* index columns a model declares, which a spatial conjunct either
|
|
128
|
+
* translates to exactly or is proven to IMPLY. `bboxOverlap` is true
|
|
129
|
+
* when the row's stored box meets the probe's (touching edges count,
|
|
130
|
+
* as the kernel's `bboxIntersects` does); `cellIn` when the row's
|
|
131
|
+
* cell is one of the listed ones (the nine-cell neighbourhood, or a
|
|
132
|
+
* single whole cell); `cellPrefix` when it begins with a shorter one.
|
|
133
|
+
* None carries a `json_type` guard — the derived column IS the value
|
|
134
|
+
* — but each is TOTAL through its own `IS NOT NULL`, so a row with no
|
|
135
|
+
* box or no cell answers FALSE rather than SQL's NULL and negation
|
|
136
|
+
* still composes classically.
|
|
101
137
|
*
|
|
102
138
|
* @typedef {{ ref: PlanRef, desc: boolean, emptyGreatest: boolean }} PlanOrderTerm
|
|
103
139
|
*
|
package/dist/types/ddl.d.ts
CHANGED
|
@@ -27,10 +27,20 @@ export declare function compileIndexPath(expression: string, docPath: string): {
|
|
|
27
27
|
canonical: string;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
* The
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* The schema subschema at a segment path, walked structurally through
|
|
31
|
+
* `properties` / `items` / `prefixItems`. The collection's schema is
|
|
32
|
+
* the type source — that is why the physical mapping needs no
|
|
33
|
+
* engine-side inference. `undefined` where the walk leaves the schema.
|
|
34
|
+
* @param {any} schema
|
|
35
|
+
* @param {import('./dialect.js').JsonPathSegment[]} segments
|
|
36
|
+
* @returns {any}
|
|
37
|
+
*/
|
|
38
|
+
export declare function schemaNodeAt(schema: any, segments: import('./dialect.js').JsonPathSegment[]): any;
|
|
39
|
+
/**
|
|
40
|
+
* The declared schema type at a segment path: the first non-`null`
|
|
41
|
+
* member of a union, which is the type a COLUMN takes its storage
|
|
42
|
+
* from. A caller that must know the whole union (a promotion refusing
|
|
43
|
+
* a member that may also be `null`) reads {@link schemaNodeAt}.
|
|
34
44
|
* @param {any} schema
|
|
35
45
|
* @param {import('./dialect.js').JsonPathSegment[]} segments
|
|
36
46
|
* @returns {string | undefined}
|
|
@@ -40,16 +50,29 @@ export declare function schemaTypeAt(schema: any, segments: import('./dialect.js
|
|
|
40
50
|
* Plan one collection's physical shape: the DDL statements to create
|
|
41
51
|
* it and the structural facts an existing table must match (the
|
|
42
52
|
* `JD0002` comparison set).
|
|
53
|
+
* A derived index (`derive: 'geohash' | 'bbox'`) maps to the same
|
|
54
|
+
* shape through a registered deterministic function, EXCEPT where the
|
|
55
|
+
* driver cannot index one (`capabilities.deterministicIndexableFunctions`
|
|
56
|
+
* is false): there the columns are ordinary ones the store writes. The
|
|
57
|
+
* two mappings produce different declared text on purpose — a database
|
|
58
|
+
* built under one and opened under the other really does disagree, and
|
|
59
|
+
* `verifyShape` says so rather than papering over it.
|
|
43
60
|
* @param {string} name - The collection name (also the table name)
|
|
44
61
|
* @param {{ schema: any, keySegments: { name: string }[] | null,
|
|
45
62
|
* identity: string, indexes: { name: string, paths: string[],
|
|
46
|
-
* unique: boolean,
|
|
63
|
+
* unique: boolean, derive?: string | null, precision?: number,
|
|
64
|
+
* docPath: string }[] }} collection - normalized
|
|
47
65
|
* @param {any} dialect
|
|
66
|
+
* @param {{ derived?: 'virtual' | 'stored' }} [options] - the physical
|
|
67
|
+
* mapping for derived columns; `'virtual'` (a generated column over a
|
|
68
|
+
* registered function) unless the driver says it cannot index one
|
|
48
69
|
* @returns {{
|
|
49
70
|
* table: string, keyColumn: string, docColumn: string,
|
|
50
71
|
* keyType: string,
|
|
51
72
|
* generated: { name: string, type: string, pathText: string,
|
|
52
73
|
* canonical: string }[],
|
|
74
|
+
* derived: { name: string, derive: string, precision?: number,
|
|
75
|
+
* component?: string, segments: any[] }[],
|
|
53
76
|
* columnByCanonical: Map<string, string>,
|
|
54
77
|
* createSql: string[],
|
|
55
78
|
* expected: { columns: { name: string, type: string,
|
|
@@ -67,9 +90,13 @@ export declare function planCollection(name: string, collection: {
|
|
|
67
90
|
name: string;
|
|
68
91
|
paths: string[];
|
|
69
92
|
unique: boolean;
|
|
93
|
+
derive?: string | null;
|
|
94
|
+
precision?: number;
|
|
70
95
|
docPath: string;
|
|
71
96
|
}[];
|
|
72
|
-
}, dialect: any
|
|
97
|
+
}, dialect: any, options?: {
|
|
98
|
+
derived?: 'virtual' | 'stored';
|
|
99
|
+
}): {
|
|
73
100
|
table: string;
|
|
74
101
|
keyColumn: string;
|
|
75
102
|
docColumn: string;
|
|
@@ -80,6 +107,13 @@ export declare function planCollection(name: string, collection: {
|
|
|
80
107
|
pathText: string;
|
|
81
108
|
canonical: string;
|
|
82
109
|
}[];
|
|
110
|
+
derived: {
|
|
111
|
+
name: string;
|
|
112
|
+
derive: string;
|
|
113
|
+
precision?: number;
|
|
114
|
+
component?: string;
|
|
115
|
+
segments: any[];
|
|
116
|
+
}[];
|
|
83
117
|
columnByCanonical: Map<string, string>;
|
|
84
118
|
createSql: string[];
|
|
85
119
|
expected: {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Derived index columns: the one place a declared
|
|
3
|
+
* `indexes[].derive` becomes a value. A spatial member is an array of
|
|
4
|
+
* numbers or an object, and a generated column must be a scalar, so a
|
|
5
|
+
* geohash cell or a bounding-box edge is what actually gets indexed.
|
|
6
|
+
*
|
|
7
|
+
* Every cell and every box comes from `@jarenjs/core/geo`; nothing
|
|
8
|
+
* here computes spatial arithmetic of its own. The same functions
|
|
9
|
+
* serve BOTH physical mappings — registered as deterministic SQL
|
|
10
|
+
* functions inside a virtual generated column's expression where the
|
|
11
|
+
* driver can index them, and called directly on the write path where
|
|
12
|
+
* it cannot — so the two branches cannot drift into different answers.
|
|
13
|
+
*
|
|
14
|
+
* It is also this package's ONLY seam onto `@jarenjs/core/geo` (D1 —
|
|
15
|
+
* one home for spatial arithmetic, grep-proven by test): the planner's
|
|
16
|
+
* probe geometry — the box of a literal or bound region, the box of a
|
|
17
|
+
* bounded-distance circle, a cell's neighbourhood — is computed by the
|
|
18
|
+
* helpers below rather than by an import of its own.
|
|
19
|
+
*
|
|
20
|
+
* Determinism is the contract, not a convenience: a value here is a
|
|
21
|
+
* pure function of the document bytes. Nothing reads the clock, a
|
|
22
|
+
* random source or store state, because an INDEX over a function that
|
|
23
|
+
* did would make the database unreadable from a connection whose
|
|
24
|
+
* function answered differently — and unreadable, not merely wrong:
|
|
25
|
+
* a connection that has not registered the function at all cannot even
|
|
26
|
+
* SELECT the table (probed). That hazard is why the mapping is
|
|
27
|
+
* capability-gated rather than always-on.
|
|
28
|
+
*/
|
|
29
|
+
/** The closed set of derive kinds. */
|
|
30
|
+
export declare const DERIVE_KINDS: Set<string>;
|
|
31
|
+
/** A bbox index's four columns, in the order they are declared —
|
|
32
|
+
* `[west, south, east, north]`, the order the kernel's boxes carry. */
|
|
33
|
+
export declare const BBOX_COMPONENTS: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* The order a bbox index COVERS its four columns, which is not the
|
|
36
|
+
* order they are declared in: an intersection test reads
|
|
37
|
+
* `w <= ? AND e >= ? AND s <= ? AND n >= ?`, so the two longitude
|
|
38
|
+
* bounds sit together at the front of the index where a leading-column
|
|
39
|
+
* range can use them. `(a,b)` and `(b,a)` are different indexes.
|
|
40
|
+
*/
|
|
41
|
+
export declare const BBOX_INDEX_ORDER: readonly string[];
|
|
42
|
+
/** The declared geohash precision range (characters). */
|
|
43
|
+
export declare const PRECISION_MIN = 1;
|
|
44
|
+
export declare const PRECISION_MAX = 12;
|
|
45
|
+
/**
|
|
46
|
+
* The geohash cell of a value at a precision, or null when the value
|
|
47
|
+
* carries no bounded position.
|
|
48
|
+
* @param {any} value
|
|
49
|
+
* @param {number} precision
|
|
50
|
+
* @returns {string | null}
|
|
51
|
+
*/
|
|
52
|
+
export declare function deriveGeohash(value: any, precision: number): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* One edge of a value's bounding box, or null when it has none —
|
|
55
|
+
* including the D6 case where a non-finite coordinate refuses the box
|
|
56
|
+
* rather than producing one that does not bound its input.
|
|
57
|
+
* @param {any} value
|
|
58
|
+
* @param {string} component - `'w'`, `'s'`, `'e'` or `'n'`
|
|
59
|
+
* @returns {number | null}
|
|
60
|
+
*/
|
|
61
|
+
export declare function deriveBboxEdge(value: any, component: string): number | null;
|
|
62
|
+
/**
|
|
63
|
+
* A member as the database will hold it. A derived value must be a
|
|
64
|
+
* function of the STORED document, not of the object handed to the
|
|
65
|
+
* write: JSON has no `NaN` and no `Infinity`, so a non-finite
|
|
66
|
+
* coordinate becomes `null` on the way in and is no longer a position
|
|
67
|
+
* at all. Computing from the in-memory value would make the two
|
|
68
|
+
* physical mappings answer differently for the same document, which is
|
|
69
|
+
* the one thing they may never do.
|
|
70
|
+
*
|
|
71
|
+
* Only the indexed MEMBER round-trips, not the whole document — it is
|
|
72
|
+
* the only part a derived column reads.
|
|
73
|
+
* @param {any} member
|
|
74
|
+
* @returns {any} the member as stored, or `undefined` when there is none
|
|
75
|
+
*/
|
|
76
|
+
export declare function storedMemberForm(member: any): any;
|
|
77
|
+
/**
|
|
78
|
+
* The value of one derived column for a document member.
|
|
79
|
+
* @param {{ derive: string, precision?: number, component?: string }} column
|
|
80
|
+
* @param {any} member - the value at the index path, or `undefined`
|
|
81
|
+
* @returns {string | number | null}
|
|
82
|
+
*/
|
|
83
|
+
export declare function derivedValue(column: {
|
|
84
|
+
derive: string;
|
|
85
|
+
precision?: number;
|
|
86
|
+
component?: string;
|
|
87
|
+
}, member: any): string | number | null;
|
|
88
|
+
/**
|
|
89
|
+
* Read the member one derived column is computed from, walking the
|
|
90
|
+
* same typed segments the physical mapping was planned over.
|
|
91
|
+
* @param {any} doc
|
|
92
|
+
* @param {import('./dialect.js').JsonPathSegment[]} segments
|
|
93
|
+
* @returns {any} the member, or `undefined`
|
|
94
|
+
*/
|
|
95
|
+
export declare function memberAt(doc: any, segments: import('./dialect.js').JsonPathSegment[]): any;
|
|
96
|
+
/**
|
|
97
|
+
* Register the deterministic functions the virtual generated columns
|
|
98
|
+
* call. Idempotent per connection by SQLite's own semantics (a second
|
|
99
|
+
* registration replaces the first with the identical implementation),
|
|
100
|
+
* and a no-op where the driver cannot index a registered function —
|
|
101
|
+
* that branch stores the columns instead.
|
|
102
|
+
*
|
|
103
|
+
* The four bbox edges share a one-entry memo of the last text they
|
|
104
|
+
* were asked about, because they are called back to back with the same
|
|
105
|
+
* argument for one row and the box costs a full walk of the geometry.
|
|
106
|
+
* The memo is keyed on the argument itself, so it changes no answer.
|
|
107
|
+
* @param {any} connection
|
|
108
|
+
* @returns {any} value-or-promise
|
|
109
|
+
*/
|
|
110
|
+
export declare function registerDeriveFunctions(connection: any): any;
|
|
111
|
+
/**
|
|
112
|
+
* The bounding box of a probe value, or `null` when it has none — the
|
|
113
|
+
* D6 refusal included, which is what makes an unbounded probe divert to
|
|
114
|
+
* the full scan instead of narrowing with a box that does not bound it.
|
|
115
|
+
* @param {any} value - a GeoJSON value or a `[lon, lat]` position
|
|
116
|
+
* @returns {number[] | null} `[west, south, east, north]`
|
|
117
|
+
*/
|
|
118
|
+
export declare function probeBox(value: any): number[] | null;
|
|
119
|
+
/**
|
|
120
|
+
* The representative position §8.14 measures a probe value by — the
|
|
121
|
+
* same rule the derived columns use, so the pushed filter and the
|
|
122
|
+
* engine cannot disagree about where a value IS. `null` when it has no
|
|
123
|
+
* bounded position.
|
|
124
|
+
* @param {any} value
|
|
125
|
+
* @returns {number[] | null}
|
|
126
|
+
*/
|
|
127
|
+
export declare function probePosition(value: any): number[] | null;
|
|
128
|
+
/**
|
|
129
|
+
* The bounding box of the circle of `metres` around a position — the
|
|
130
|
+
* box a `$distance <= r` predicate narrows with, on the same sphere and
|
|
131
|
+
* the same radius constant the engine measures with. `null` when the
|
|
132
|
+
* radius is not a finite non-negative number, or when the circle
|
|
133
|
+
* reaches a pole, where there is no longitude bound to give.
|
|
134
|
+
* @param {number[]} position
|
|
135
|
+
* @param {number} metres
|
|
136
|
+
* @returns {number[] | null} `[west, south, east, north]`, NOT wrapped
|
|
137
|
+
* into `[-180, 180]`: a circle spanning the antimeridian answers a
|
|
138
|
+
* west below -180, which is how the planner detects it
|
|
139
|
+
*/
|
|
140
|
+
export declare function probeCircleBox(position: number[], metres: number): number[] | null;
|
|
141
|
+
/**
|
|
142
|
+
* A cell and its neighbours, the nine-cell probe D7 requires — a single
|
|
143
|
+
* prefix is bucketing, never proximity.
|
|
144
|
+
* @param {string} cell
|
|
145
|
+
* @returns {string[]} up to nine cells (fewer past a pole)
|
|
146
|
+
*/
|
|
147
|
+
export declare function cellNeighbourhood(cell: string): string[];
|
|
148
|
+
/**
|
|
149
|
+
* The value one DERIVED parameter slot binds: an axis of a bound
|
|
150
|
+
* external's bounding box, computed at bind time because a GeoJSON
|
|
151
|
+
* object is not a value any database can bind. `null` when the value
|
|
152
|
+
* has no box, which is what tells the caller to divert.
|
|
153
|
+
* @param {{ kind: string, external: string, axis: string }} derived
|
|
154
|
+
* @param {any} value - the bound external
|
|
155
|
+
* @returns {number | null}
|
|
156
|
+
*/
|
|
157
|
+
export declare function derivedSlotValue(derived: {
|
|
158
|
+
kind: string;
|
|
159
|
+
external: string;
|
|
160
|
+
axis: string;
|
|
161
|
+
}, value: any): number | null;
|
package/dist/types/dialect.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export type JsonPathSegment = {
|
|
|
44
44
|
* limitClause: (limit: number, offset?: number) => string,
|
|
45
45
|
* jsonPathText: (segments: JsonPathSegment[]) => string | null,
|
|
46
46
|
* jsonExtract: (columnSql: string, pathText: string) => string,
|
|
47
|
+
* derivedExpression?: (memberSql: string, column: { derive: string,
|
|
48
|
+
* precision?: number, component?: string }) => string,
|
|
47
49
|
* jsonSet: (exprSql: string, pathText: string, valueSql: string) => string,
|
|
48
50
|
* jsonRemove: (exprSql: string, pathText: string) => string,
|
|
49
51
|
* jsonAppend: (exprSql: string, arrayPathText: string, valueSql: string) => string,
|
|
@@ -52,7 +54,8 @@ export type JsonPathSegment = {
|
|
|
52
54
|
* jsonAgg: (exprSql: string) => string,
|
|
53
55
|
* jsonTypeOf: (columnSql: string, pathText: string) => string,
|
|
54
56
|
* valueTypeOf: (paramSql: string) => string,
|
|
55
|
-
* strStartsWith: (valueSql: string,
|
|
57
|
+
* strStartsWith: (valueSql: string, lowerParamSql: string, upperParamSql: string) => string,
|
|
58
|
+
* strStartsWithExact: (valueSql: string, patternA: string, patternB: string) => string,
|
|
56
59
|
* strEndsWith: (valueSql: string, patternA: string, patternB: string, patternC: string) => string,
|
|
57
60
|
* strContains: (valueSql: string, patternSql: string) => string,
|
|
58
61
|
* orderNulls: (nullsFirst: boolean) => string,
|
|
@@ -88,6 +91,11 @@ export declare function createDialect(spec: {
|
|
|
88
91
|
limitClause: (limit: number, offset?: number) => string;
|
|
89
92
|
jsonPathText: (segments: JsonPathSegment[]) => string | null;
|
|
90
93
|
jsonExtract: (columnSql: string, pathText: string) => string;
|
|
94
|
+
derivedExpression?: (memberSql: string, column: {
|
|
95
|
+
derive: string;
|
|
96
|
+
precision?: number;
|
|
97
|
+
component?: string;
|
|
98
|
+
}) => string;
|
|
91
99
|
jsonSet: (exprSql: string, pathText: string, valueSql: string) => string;
|
|
92
100
|
jsonRemove: (exprSql: string, pathText: string) => string;
|
|
93
101
|
jsonAppend: (exprSql: string, arrayPathText: string, valueSql: string) => string;
|
|
@@ -96,7 +104,8 @@ export declare function createDialect(spec: {
|
|
|
96
104
|
jsonAgg: (exprSql: string) => string;
|
|
97
105
|
jsonTypeOf: (columnSql: string, pathText: string) => string;
|
|
98
106
|
valueTypeOf: (paramSql: string) => string;
|
|
99
|
-
strStartsWith: (valueSql: string,
|
|
107
|
+
strStartsWith: (valueSql: string, lowerParamSql: string, upperParamSql: string) => string;
|
|
108
|
+
strStartsWithExact: (valueSql: string, patternA: string, patternB: string) => string;
|
|
100
109
|
strEndsWith: (valueSql: string, patternA: string, patternB: string, patternC: string) => string;
|
|
101
110
|
strContains: (valueSql: string, patternSql: string) => string;
|
|
102
111
|
orderNulls: (nullsFirst: boolean) => string;
|
package/dist/types/driver.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* them without a contract change; pretending SQLite has them is the
|
|
29
29
|
* silent degradation this suite refuses.
|
|
30
30
|
*/
|
|
31
|
+
import { isThenable, chain, toPromise } from '@jarenjs/core/function';
|
|
31
32
|
/** The minimum SQLite the store accepts, asserted at open. */
|
|
32
33
|
export declare const SQLITE_FLOOR = "3.45.0";
|
|
33
34
|
/**
|
|
@@ -38,26 +39,7 @@ export declare const SQLITE_FLOOR = "3.45.0";
|
|
|
38
39
|
* busy timeout) or another transaction on this one.
|
|
39
40
|
*/
|
|
40
41
|
export declare const DEFAULT_QUEUE_TIMEOUT = 5000;
|
|
41
|
-
|
|
42
|
-
* @param {any} value
|
|
43
|
-
* @returns {boolean} true when the value is a thenable
|
|
44
|
-
*/
|
|
45
|
-
export declare function isThenable(value: any): boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Sync-capable-async composition: apply `next` to a driver result
|
|
48
|
-
* without allocating a promise when the result is already a value.
|
|
49
|
-
* @param {any} value - A driver return: a value or a promise
|
|
50
|
-
* @param {(value: any) => any} next
|
|
51
|
-
* @returns {any} `next`'s result, promise-wrapped only if the input was
|
|
52
|
-
*/
|
|
53
|
-
export declare function chain(value: any, next: (value: any) => any): any;
|
|
54
|
-
/**
|
|
55
|
-
* Lift a driver result into a promise — the ONE allocation the public
|
|
56
|
-
* asynchronous surface pays per call.
|
|
57
|
-
* @param {any} value
|
|
58
|
-
* @returns {Promise<any>}
|
|
59
|
-
*/
|
|
60
|
-
export declare function toPromise(value: any): Promise<any>;
|
|
42
|
+
export { isThenable, chain, toPromise };
|
|
61
43
|
/**
|
|
62
44
|
* Compare two dotted version strings numerically.
|
|
63
45
|
* @param {string} a
|
package/dist/types/emit.d.ts
CHANGED
|
@@ -18,10 +18,13 @@ export type ParamSlot = {
|
|
|
18
18
|
external: string;
|
|
19
19
|
} | {
|
|
20
20
|
literal: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
derived: {
|
|
23
|
+
kind: 'bboxAxis';
|
|
24
|
+
external: string;
|
|
25
|
+
axis: 'w' | 's' | 'e' | 'n';
|
|
26
|
+
};
|
|
21
27
|
};
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {{ external: string } | { literal: unknown }} ParamSlot
|
|
24
|
-
*/
|
|
25
28
|
/**
|
|
26
29
|
* Emit one plan as SQL plus its ordered parameter slots.
|
|
27
30
|
* @param {import('./algebra.js').Plan} plan
|