@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/dist/types/errors.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const DB_CODES: Readonly<{
|
|
|
19
19
|
JD0001: "the SQLite library is below the supported floor";
|
|
20
20
|
JD0002: "the declared model disagrees with the existing database";
|
|
21
21
|
JD0003: "the driver binding is unavailable on this runtime";
|
|
22
|
-
JD0004: "
|
|
22
|
+
JD0004: "a declared index cannot be mapped to a column";
|
|
23
23
|
JD0005: "the model document is invalid";
|
|
24
24
|
JD0010: "strict mode refused a residual";
|
|
25
25
|
JD0011: "the profile refused the document";
|
|
@@ -62,9 +62,11 @@ export declare const DB_CODES: Readonly<{
|
|
|
62
62
|
* - `JD0003` — the runtime builtin behind a driver could not be
|
|
63
63
|
* loaded here (Node cannot resolve `bun:`; Bun ships no
|
|
64
64
|
* `node:sqlite`), or an injected handle is missing
|
|
65
|
-
* - `JD0004` —
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* - `JD0004` — a declared index cannot be mapped to a column: its
|
|
66
|
+
* path does not select exactly one member (wildcards, slices,
|
|
67
|
+
* filters and descendants are not indexable), or its `derive`
|
|
68
|
+
* declaration is not one the storage vocabulary carries; the reason
|
|
69
|
+
* names the expression or the member and `docPath` points at it
|
|
68
70
|
* - `JD0005` — the model document is invalid; `docPath` points at
|
|
69
71
|
* the offending member
|
|
70
72
|
* - `JD0010` — `strict: true` and part of the query would have run
|
package/dist/types/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { selectPlan, conjoin, assertNoSqlText, PLAN_VERSION } from './algebra.js
|
|
|
18
18
|
export { typeOfPath, isNumericType } from './types.js';
|
|
19
19
|
export { compileSetResidual, compileRowResidual, sequenceResult } from './residual.js';
|
|
20
20
|
export { deterministicFragment, registerFragment } from './udf.js';
|
|
21
|
+
export { DERIVE_KINDS, BBOX_COMPONENTS, BBOX_INDEX_ORDER, PRECISION_MIN, PRECISION_MAX, deriveGeohash, deriveBboxEdge, derivedValue, memberAt, storedMemberForm, registerDeriveFunctions, } from './derive.js';
|
|
21
22
|
export { createQueryEngine, createQueryState, createEntityQueryEngine, createLoadEngine, INCLUDE_DEPTH_DEFAULT, } from './query.js';
|
|
22
23
|
export { normalizeProfile, SAFE_PROFILE, translateProfilePredicate, applyMandatoryPredicate, applyRowBound, } from './profile.js';
|
|
23
24
|
export { translatePatch } from './patch-sql.js';
|
package/dist/types/migrate.d.ts
CHANGED
|
@@ -43,9 +43,14 @@ export declare function migrationChecksum(migration: any): string;
|
|
|
43
43
|
* refuses to run until the author fills it in — the planner cannot
|
|
44
44
|
* infer a data transform and does not pretend to. Renames are declared
|
|
45
45
|
* (`x-rename` on the target collection), never guessed.
|
|
46
|
+
* The physical mapping of a DERIVED index column depends on the driver
|
|
47
|
+
* that will run the migration (`derived`), because the two mappings
|
|
48
|
+
* really are different columns; a migration document planned for one is
|
|
49
|
+
* not the document the other needs.
|
|
46
50
|
* @param {any} fromModel
|
|
47
51
|
* @param {any} toModel
|
|
48
|
-
* @param {{ id?: string, dialect?: any
|
|
52
|
+
* @param {{ id?: string, dialect?: any,
|
|
53
|
+
* derived?: 'virtual' | 'stored' }} [options]
|
|
49
54
|
* @returns {{ migration: any, report: {
|
|
50
55
|
* renamed: { from: string, to: string }[],
|
|
51
56
|
* added: string[], removed: string[],
|
|
@@ -55,6 +60,7 @@ export declare function migrationChecksum(migration: any): string;
|
|
|
55
60
|
export declare function planMigration(fromModel: any, toModel: any, options?: {
|
|
56
61
|
id?: string;
|
|
57
62
|
dialect?: any;
|
|
63
|
+
derived?: 'virtual' | 'stored';
|
|
58
64
|
}): {
|
|
59
65
|
migration: any;
|
|
60
66
|
report: {
|
package/dist/types/plan.d.ts
CHANGED
|
@@ -10,16 +10,24 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The outcome of planning one document:
|
|
12
12
|
*
|
|
13
|
-
* { plan, mode: 'native' | 'row' | 'set', reasons, rowReturn
|
|
13
|
+
* { plan, mode: 'native' | 'row' | 'set', reasons, rowReturn,
|
|
14
|
+
* prefilters }
|
|
14
15
|
*
|
|
15
16
|
* - `native` — everything translated; the plan alone answers.
|
|
16
17
|
* - `row` — predicates, ordering and window pushed; only the
|
|
17
18
|
* projection runs in the engine, per fetched row (streams).
|
|
19
|
+
* `rowReturn` is the COMPLETE one-row document to run, binding
|
|
20
|
+
* included — the collection binding is named by the document, so a
|
|
21
|
+
* wrapper built anywhere else would have to guess it.
|
|
18
22
|
* - `set` — the pushed conjuncts narrow candidates; the WHOLE
|
|
19
23
|
* compiled document runs over the materialized candidates.
|
|
20
24
|
*
|
|
21
25
|
* `reasons` names every construct that forced work off the database,
|
|
22
26
|
* with reason text drawn from the deliberate-residual table.
|
|
27
|
+
* `prefilters` names the IMPLIED conjuncts — predicates the planner
|
|
28
|
+
* ADDED because a spatial one provably implies them (see "Spatial
|
|
29
|
+
* promotions" below) — with the columns each reads and whether it
|
|
30
|
+
* decided or merely narrowed.
|
|
23
31
|
*/
|
|
24
32
|
/**
|
|
25
33
|
* Assert a node kind is one this planner has decided. Called on every
|
|
@@ -45,6 +53,7 @@ export declare function assertDecidedKind(node: any): void;
|
|
|
45
53
|
* reasons: { construct: string, reason: string }[],
|
|
46
54
|
* rowReturn: any,
|
|
47
55
|
* udfs: string[],
|
|
56
|
+
* prefilters: { construct: string, columns: string[], exact: boolean }[],
|
|
48
57
|
* }}
|
|
49
58
|
*/
|
|
50
59
|
export declare function planQuery(document: any, shape: any, options?: {
|
|
@@ -62,6 +71,11 @@ export declare function planQuery(document: any, shape: any, options?: {
|
|
|
62
71
|
}[];
|
|
63
72
|
rowReturn: any;
|
|
64
73
|
udfs: string[];
|
|
74
|
+
prefilters: {
|
|
75
|
+
construct: string;
|
|
76
|
+
columns: string[];
|
|
77
|
+
exact: boolean;
|
|
78
|
+
}[];
|
|
65
79
|
};
|
|
66
80
|
/**
|
|
67
81
|
* Build the planner shape for one entity: canonical top-level paths
|
package/dist/types/residual.d.ts
CHANGED
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* materialized candidate array. Re-applying pushed conjuncts is
|
|
10
10
|
* idempotent, so SQL-side narrowing never changes the answer.
|
|
11
11
|
* - `row` — only the projection stayed behind: each fetched row runs
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* the planner's one-row document over the one-row array; the array
|
|
13
|
+
* wrapper packs the item sequence so an array-VALUED item stays
|
|
14
|
+
* unambiguous, and the per-row results concatenate in row order
|
|
15
|
+
* (streamable).
|
|
16
|
+
*
|
|
17
|
+
* Neither mode builds a query document here. The collection binding is
|
|
18
|
+
* named by the caller's document — `it`, `user`, anything — so a wrapper
|
|
19
|
+
* synthesized in this file could only guess it, and a guess that
|
|
20
|
+
* disagreed with the projection's references would surface as an
|
|
21
|
+
* unbound-external error at request time rather than at compile time.
|
|
22
|
+
* The planner knows the name and hands both modes something complete.
|
|
16
23
|
*/
|
|
17
24
|
/**
|
|
18
25
|
* Compile the whole document for set-mode evaluation. A profile's
|
|
@@ -30,12 +37,16 @@ export declare function compileSetResidual(document: any, limits?: any, operator
|
|
|
30
37
|
} | null): (candidates: any[], externals: any) => any;
|
|
31
38
|
/**
|
|
32
39
|
* Compile the per-row projection for row-mode evaluation.
|
|
33
|
-
* @param {any}
|
|
40
|
+
* @param {any} rowDocument - The planner's complete one-row document
|
|
41
|
+
* (`{ $for: { <the document's own binding>: '$[*]' },
|
|
42
|
+
* $return: [ <its $return> ] }`). It arrives whole because the binding
|
|
43
|
+
* and the projection that references it must agree, and the planner is
|
|
44
|
+
* the only place that knows the name.
|
|
34
45
|
* @param {any} [limits]
|
|
35
46
|
* @param {{ functions?: any, extensions?: any } | null} [operators]
|
|
36
47
|
* @returns {(row: any, externals: any) => any[]} the row's items
|
|
37
48
|
*/
|
|
38
|
-
export declare function compileRowResidual(
|
|
49
|
+
export declare function compileRowResidual(rowDocument: any, limits?: any, operators?: {
|
|
39
50
|
functions?: any;
|
|
40
51
|
extensions?: any;
|
|
41
52
|
} | null): (row: any, externals: any) => any[];
|
package/dist/types/udf.d.ts
CHANGED
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
* [operators] - the store's registered operators (Ring 3); only its
|
|
40
40
|
* `pushable:'scalar'` subset is admitted. `null`/absent keeps the
|
|
41
41
|
* original engine-internal-only rule (no host function pushes).
|
|
42
|
+
* @param {string} [binding='it'] - the name the caller's document gave
|
|
43
|
+
* the collection binding. The fragment references it, so the wrapper
|
|
44
|
+
* below must bind it: under any other name every reference reads as an
|
|
45
|
+
* external, the determinism check below rejects the fragment, and the
|
|
46
|
+
* hatch silently never engages.
|
|
42
47
|
* @returns {{ key: string, name: string,
|
|
43
48
|
* compile: () => (docText: string) => number } | null}
|
|
44
49
|
*/
|
|
@@ -46,7 +51,7 @@ export declare function deterministicFragment(fragment: any, operators?: {
|
|
|
46
51
|
functions?: any;
|
|
47
52
|
extensions?: any;
|
|
48
53
|
pushableScalar?: Set<string>;
|
|
49
|
-
} | null): {
|
|
54
|
+
} | null, binding?: string): {
|
|
50
55
|
key: string;
|
|
51
56
|
name: string;
|
|
52
57
|
compile: () => (docText: string) => number;
|
package/docs/MIGRATION-FORMAT.md
CHANGED
|
@@ -53,11 +53,40 @@ shape change is a **transformation of values**, not a table rebuild.
|
|
|
53
53
|
the migration to proceed. This is how a migration states its own
|
|
54
54
|
precondition — "no user has a null email before the NOT NULL
|
|
55
55
|
index" — and it is checked on the shadow first.
|
|
56
|
+
- `kind: "derive"` recomputes named STORED derived index columns from
|
|
57
|
+
the documents already in a collection — the backfill described in
|
|
58
|
+
§2.1. It is idempotent: a derived value is a pure function of the
|
|
59
|
+
document, so a replay writes what the first run wrote.
|
|
56
60
|
- Steps are ordered, and the order is the contract.
|
|
57
61
|
|
|
62
|
+
### 2.1 Derived spatial columns and the backfill
|
|
63
|
+
|
|
64
|
+
MODEL-FORMAT §3.1 gives a derived index (`derive: 'geohash' | 'bbox'`)
|
|
65
|
+
two physical mappings, chosen by what the driver declares. That choice
|
|
66
|
+
belongs to the migration DOCUMENT, because the two mappings really are
|
|
67
|
+
different columns: `planMigration(from, to, { dialect, derived })`
|
|
68
|
+
takes `'virtual'` (the default: a generated column) or `'stored'`, and
|
|
69
|
+
a document planned for one is not the document the other needs.
|
|
70
|
+
|
|
71
|
+
The difference the runner sees is one step. A generated column arrives
|
|
72
|
+
POPULATED — SQLite computes it from every existing row. A stored one
|
|
73
|
+
arrives `NULL`, and a query pushed to a `NULL` column silently returns
|
|
74
|
+
fewer rows, so the planner emits an explicit `derive` step after the
|
|
75
|
+
`ALTER TABLE … ADD COLUMN`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{ "kind": "derive", "collection": "places",
|
|
79
|
+
"columns": [ { "name": "gx_at_gh7", "derive": "geohash", "precision": 7,
|
|
80
|
+
"segments": [ { "name": "at" } ] } ] }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A `jslt` transform on such a collection gets the same treatment for the
|
|
84
|
+
same reason: it rewrites the documents the columns are computed from,
|
|
85
|
+
so the planner follows it with a `derive` step that recomputes them.
|
|
86
|
+
|
|
58
87
|
## 3. Planning and the widening/narrowing rule
|
|
59
88
|
|
|
60
|
-
`planMigration(fromModel, toModel, { dialect, id })` produces
|
|
89
|
+
`planMigration(fromModel, toModel, { dialect, id, derived })` produces
|
|
61
90
|
`{ migration, report }` by diffing the two models' PHYSICAL plans:
|
|
62
91
|
|
|
63
92
|
- An added collection becomes its full CREATE DDL; a removed
|
package/docs/MODEL-FORMAT.md
CHANGED
|
@@ -64,10 +64,89 @@ The store runs over SQLite — on Node (`@jarenjs/db/node`), on Bun
|
|
|
64
64
|
non-singular path is rejected at open with `JD0004` naming the
|
|
65
65
|
expression. A composite index takes a non-empty array of paths.
|
|
66
66
|
- Index names MUST be identifiers, unique within their collection.
|
|
67
|
+
- `indexes[].derive` declares a **derived** index: the columns are
|
|
68
|
+
computed FROM the selected member rather than being the member.
|
|
69
|
+
See §2.1.
|
|
67
70
|
|
|
68
71
|
An invalid model document is `JD0005` with a `docPath` pointing at the
|
|
69
72
|
offending member. Model checking happens before any database work.
|
|
70
73
|
|
|
74
|
+
### 2.1 Derived indexes (spatial storage)
|
|
75
|
+
|
|
76
|
+
A generated column must be a scalar (§3), and a GeoJSON position is an
|
|
77
|
+
array of numbers while a geometry is an object. No path over spatial
|
|
78
|
+
data is therefore indexable as written. `derive` supplies the missing
|
|
79
|
+
vocabulary: it says what indexable scalar is computed from the member.
|
|
80
|
+
|
|
81
|
+
```jsonc
|
|
82
|
+
"indexes": [
|
|
83
|
+
{ "name": "by_cell", "path": "$.at", "derive": "geohash", "precision": 7 },
|
|
84
|
+
{ "name": "by_box", "path": "$.geometry", "derive": "bbox" }
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| `derive` | `path` selects | `precision` | columns | type |
|
|
89
|
+
|---|---|---|---|---|
|
|
90
|
+
| `"geohash"` | a position `[lon, lat]`, a `Point`, or any value with a representative position (the mean of its vertices) | 1..12, **required** | one, `<column>` | `TEXT` |
|
|
91
|
+
| `"bbox"` | any GeoJSON value | — | four: `<column>_w`, `<column>_s`, `<column>_e`, `<column>_n` | `REAL` |
|
|
92
|
+
|
|
93
|
+
`derive` is a CLOSED set of those two values. An open "expression"
|
|
94
|
+
member would be a second query language inside the model document,
|
|
95
|
+
which this format does not have and will not grow.
|
|
96
|
+
|
|
97
|
+
Every rule below is `JD0004` with a `docPath` at the offending member:
|
|
98
|
+
|
|
99
|
+
1. **`precision` is required for `geohash`, and refused anywhere
|
|
100
|
+
else.** There is no safe default, in either direction — the table
|
|
101
|
+
below is the reason: precision 9 is a ~4.8 m cell (a very large
|
|
102
|
+
index for city-scale work) and precision 4 is ~20 km. The right
|
|
103
|
+
value follows from the query radius, which the model cannot know.
|
|
104
|
+
2. **A derived index is never `unique`.** Two distinct positions share
|
|
105
|
+
a cell — and share a box edge — by construction.
|
|
106
|
+
3. **The path must still be singular, and there must be exactly one of
|
|
107
|
+
it.** `derive` changes what is computed from the member, never how
|
|
108
|
+
the member is selected: a wildcard path is refused exactly as it is
|
|
109
|
+
for an undecorated index, and a composite (array) path is refused
|
|
110
|
+
because a derivation reads ONE member.
|
|
111
|
+
4. **The schema is still the type source.** A `derive` over a path the
|
|
112
|
+
collection's schema types as `string`, `integer`, `number` or
|
|
113
|
+
`boolean` is refused: the mistake is worth catching at open rather
|
|
114
|
+
than at the first query that quietly returns nothing. A path the
|
|
115
|
+
schema does not type is accepted — there is nothing to contradict —
|
|
116
|
+
but **a spatial predicate is only PUSHED onto a column whose member
|
|
117
|
+
the schema types as an array or an object** (a union of the two is
|
|
118
|
+
fine; one that also admits `null` is not), because §8.14 raises for
|
|
119
|
+
a non-geographic operand and a pushed filter would simply not see
|
|
120
|
+
the row. Declaring the type is what turns a derived index from
|
|
121
|
+
storage into a plan.
|
|
122
|
+
5. **Two indexes with the same `(path, derive, precision)` share one
|
|
123
|
+
column set**, extending §3's rule for undecorated paths. Two
|
|
124
|
+
`geohash` indexes over one path at DIFFERENT precisions are two
|
|
125
|
+
column sets, and legitimately so: a coarse bucketing index and a
|
|
126
|
+
fine proximity one are different indexes.
|
|
127
|
+
6. **A `bbox` index covers its four columns in `(w, e, s, n)` order** —
|
|
128
|
+
not the order they are declared in. An intersection test reads
|
|
129
|
+
`w <= ? AND e >= ? AND s <= ? AND n >= ?`, so the two longitude
|
|
130
|
+
bounds sit together at the front of the index where a leading-column
|
|
131
|
+
range can use them; `(a,b)` and `(b,a)` are different indexes (§3).
|
|
132
|
+
|
|
133
|
+
Approximate geohash cell size by precision (the kernel's
|
|
134
|
+
`geohashCellSize` computes the degree figures; the metric ones are at
|
|
135
|
+
the equator):
|
|
136
|
+
|
|
137
|
+
| precision | cell (lon × lat) | precision | cell (lon × lat) |
|
|
138
|
+
|---|---|---|---|
|
|
139
|
+
| 1 | 5009 km × 4976 km | 7 | 153 m × 152 m |
|
|
140
|
+
| 2 | 1252 km × 622 km | 8 | 38 m × 19 m |
|
|
141
|
+
| 3 | 157 km × 155 km | 9 | 4.8 m × 4.7 m |
|
|
142
|
+
| 4 | 39.1 km × 19.4 km | 10 | 1.2 m × 59 cm |
|
|
143
|
+
| 5 | 4.9 km × 4.9 km | 11 | 14.9 cm × 14.8 cm |
|
|
144
|
+
| 6 | 1.2 km × 607 m | 12 | 3.7 cm × 1.9 cm |
|
|
145
|
+
|
|
146
|
+
A geohash prefix is **bucketing, not proximity**: two points metres
|
|
147
|
+
apart can sit in different cells, so a proximity probe tests the cell's
|
|
148
|
+
neighbourhood (`geohashNeighbours`) rather than the single cell.
|
|
149
|
+
|
|
71
150
|
## 3. Physical mapping
|
|
72
151
|
|
|
73
152
|
Each collection maps to one table, rendered entirely by the dialect —
|
|
@@ -124,6 +203,69 @@ Physical column ORDER is deliberately **not** drift. SQLite's
|
|
|
124
203
|
freshly built one legitimately disagree there, and this store never reads
|
|
125
204
|
a column positionally.
|
|
126
205
|
|
|
206
|
+
### 3.1 Derived columns, and the capability branch
|
|
207
|
+
|
|
208
|
+
A derived column (§2.1) needs a function SQLite does not have, so its
|
|
209
|
+
mapping BRANCHES on what the driver declares. That is what the
|
|
210
|
+
capability table (§4) is for: a driver that cannot do a thing says so
|
|
211
|
+
rather than degrading silently.
|
|
212
|
+
|
|
213
|
+
| `capabilities.deterministicIndexableFunctions` | mapping | drivers |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `true` | a **virtual generated column** whose expression calls a deterministic function the store registers at open — `jaren_geohash(<member>, <precision>)`, `jaren_bbox_w(<member>)`, … over `json(jsonb_extract("doc", '<path>'))` | `node`, `wasm` |
|
|
216
|
+
| `false` | a **stored column** the store writes on every insert, upsert and patch, computed in JavaScript from the same kernel call | `bun` |
|
|
217
|
+
|
|
218
|
+
Three consequences, each normative:
|
|
219
|
+
|
|
220
|
+
- **The registered function MUST be deterministic** in the strong
|
|
221
|
+
sense: the same document bytes give the same cell, forever. It reads
|
|
222
|
+
the member and never the clock, a random source or store state. That
|
|
223
|
+
is not a style rule. An INDEX over a registered function makes the
|
|
224
|
+
table unreadable from a connection that has not registered an
|
|
225
|
+
identical function — not merely wrong, unreadable: a bare `SELECT`
|
|
226
|
+
fails with `unknown function`. That hazard is exactly why the mapping
|
|
227
|
+
is capability-gated rather than always-on, and why the store, the
|
|
228
|
+
migration runner and the shadow database each register these
|
|
229
|
+
functions before any statement over such a table.
|
|
230
|
+
- **A derived value is a function of the STORED document.** JSON has no
|
|
231
|
+
`NaN` and no `Infinity`, so the write path computes from the member
|
|
232
|
+
as it will be held rather than from the object handed in. Without
|
|
233
|
+
that the two mappings would answer differently for one document.
|
|
234
|
+
- **The same model document produces two different physical shapes**,
|
|
235
|
+
and *match* is by declared text (above). A database created under
|
|
236
|
+
`node` and opened under `bun` therefore reports `JD0002` naming the
|
|
237
|
+
derived column — correctly: the column really is different. The
|
|
238
|
+
physical mapping is a property of the driver that CREATED the file,
|
|
239
|
+
and moving a file between the two is a migration, not an open.
|
|
240
|
+
|
|
241
|
+
### 3.2 When a derived column is `NULL`
|
|
242
|
+
|
|
243
|
+
A derived column is SQL `NULL` when the document has no member at the
|
|
244
|
+
index path, and when the member has no bounded position — a value with
|
|
245
|
+
no positions at all, or one whose coordinates are not positions, which
|
|
246
|
+
is what a non-finite coordinate becomes: JSON cannot carry `NaN`, so it
|
|
247
|
+
arrives as `null` and is no longer a number.
|
|
248
|
+
|
|
249
|
+
The consequence is stated here rather than discovered later: **a row
|
|
250
|
+
whose derived column is `NULL` is not found by a predicate pushed to
|
|
251
|
+
that column.** For the spatial predicates the planner promotes
|
|
252
|
+
(ARCHITECTURE.md, "The implied conjunct") that is not a divergence —
|
|
253
|
+
§8.14 measures a value by its representative position, and that
|
|
254
|
+
position is missing in exactly the cases the box is, so `$within`,
|
|
255
|
+
`$bbox-intersects` and `$distance` answer `false`/empty for such a row
|
|
256
|
+
anyway. Where it does bite is the ERROR behaviour: §8.14 raises
|
|
257
|
+
`JQ2001` for an operand that is not geography at all (a string, a
|
|
258
|
+
stored `null`), and a row the pushed filter never fetched cannot
|
|
259
|
+
raise. The promotion therefore requires the schema to type the member
|
|
260
|
+
as an array or an object and nothing else; a store that wants the
|
|
261
|
+
engine's refusal instead keeps `compileSchema` injected.
|
|
262
|
+
|
|
263
|
+
Traversal and validity stay separate concerns, as they do in the
|
|
264
|
+
kernel: a value that carries SOME positions is bounded by the positions
|
|
265
|
+
it has. A `LineString` whose second vertex did not survive as a
|
|
266
|
+
position is bounded by its first — a document the GeoJSON meta-schema
|
|
267
|
+
refuses in the first place — and both mappings agree about it.
|
|
268
|
+
|
|
127
269
|
## 4. The driver contract and the synchronous fast path
|
|
128
270
|
|
|
129
271
|
A driver is `{ name, dialect, open(path, options) }`; `open` returns a
|
|
@@ -160,7 +302,10 @@ change.
|
|
|
160
302
|
On the Bun binding, `userFunctions`,
|
|
161
303
|
`deterministicIndexableFunctions` and `sessions` are `false` by
|
|
162
304
|
construction: `bun:sqlite` exposes no `function`, no `aggregate` and
|
|
163
|
-
no `createSession`.
|
|
305
|
+
no `createSession`. `deterministicIndexableFunctions` is the capability
|
|
306
|
+
the derived-column mapping branches on (§3.1), so a model that declares
|
|
307
|
+
a spatial index is portable across all three drivers and the physical
|
|
308
|
+
shape it produces is not.
|
|
164
309
|
|
|
165
310
|
A library below SQLite **3.45** fails at open with `JD0001` naming
|
|
166
311
|
the version found.
|
|
@@ -218,7 +363,9 @@ returns a validation function; the function returns `true`/`false` or
|
|
|
218
363
|
and `store.capabilities.validated === false` — a declared downgrade.
|
|
219
364
|
The cost of running without one: the database constraints only see the
|
|
220
365
|
key and the indexed members; everything else is stored as given.
|
|
221
|
-
`@jarenjs/db` never imports
|
|
366
|
+
`@jarenjs/db` never runs a validator of its own; what it imports from
|
|
367
|
+
`@jarenjs/validate` is only the pure same-document `$ref`/`$anchor`
|
|
368
|
+
resolution in `@jarenjs/validate/normalize`, for model compilation.
|
|
222
369
|
|
|
223
370
|
**`patch` validates the result, then updates in place.** The patch is
|
|
224
371
|
applied to the stored document with the copy-on-write engine and the
|
|
@@ -280,7 +427,11 @@ SQLite has no per-statement transaction scope and every operation inside
|
|
|
280
427
|
a callback reaches the connection the same way an unrelated caller does.
|
|
281
428
|
Work that must be in the transaction is therefore safe; an unrelated
|
|
282
429
|
writer on a SHARED store is not. Give each concurrent writer its own
|
|
283
|
-
store when independent writes must not share a rollback.
|
|
430
|
+
store when independent writes must not share a rollback. Closing this
|
|
431
|
+
— scope-bound `tx` handles, with store-level calls waiting on the gate
|
|
432
|
+
while a foreign scope is open — is an open ROADMAP item (`@jarenjs/db`,
|
|
433
|
+
"Strong same-store transaction ownership"); until it lands, one store
|
|
434
|
+
shared by independent request handlers is unsafe for bare writes.
|
|
284
435
|
|
|
285
436
|
## 6. Identity
|
|
286
437
|
|
|
@@ -314,7 +465,7 @@ error.
|
|
|
314
465
|
| `JD0001` | the SQLite library is below the supported floor |
|
|
315
466
|
| `JD0002` | the declared model disagrees with the existing database |
|
|
316
467
|
| `JD0003` | the driver binding is unavailable on this runtime |
|
|
317
|
-
| `JD0004` |
|
|
468
|
+
| `JD0004` | a declared index cannot be mapped to a column |
|
|
318
469
|
| `JD0005` | the model document is invalid |
|
|
319
470
|
| `JD0010` | strict mode refused a residual |
|
|
320
471
|
| `JD0011` | the profile refused the document |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.43.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"prepack": "npm run build:types"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@jarenjs/core": "^0.
|
|
75
|
-
"@jarenjs/json": "^0.
|
|
76
|
-
"@jarenjs/validate": "^0.
|
|
74
|
+
"@jarenjs/core": "^0.43.1",
|
|
75
|
+
"@jarenjs/json": "^0.43.1",
|
|
76
|
+
"@jarenjs/validate": "^0.43.1"
|
|
77
77
|
},
|
|
78
78
|
"bin": {
|
|
79
79
|
"jaren-db": "./src/cli.js"
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"$ref": "#/definitions/queryStep"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"$ref": "#/definitions/deriveStep"
|
|
58
61
|
}
|
|
59
62
|
]
|
|
60
63
|
},
|
|
@@ -139,6 +142,74 @@
|
|
|
139
142
|
"assert"
|
|
140
143
|
],
|
|
141
144
|
"additionalProperties": false
|
|
145
|
+
},
|
|
146
|
+
"deriveStep": {
|
|
147
|
+
"description": "Recompute named STORED derived index columns from the documents already in a collection. Only the physical mapping that cannot index a registered deterministic function has these columns; the generated mapping populates them itself. The step is idempotent — a derived value is a pure function of the document — so a replay writes what the first run wrote.",
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"kind": {
|
|
151
|
+
"const": "derive"
|
|
152
|
+
},
|
|
153
|
+
"collection": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
156
|
+
},
|
|
157
|
+
"columns": {
|
|
158
|
+
"description": "The columns to recompute, with the derivation and the typed member path each is computed from.",
|
|
159
|
+
"type": "array",
|
|
160
|
+
"minItems": 1,
|
|
161
|
+
"items": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"properties": {
|
|
164
|
+
"name": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"minLength": 1
|
|
167
|
+
},
|
|
168
|
+
"derive": {
|
|
169
|
+
"enum": [
|
|
170
|
+
"geohash",
|
|
171
|
+
"bbox"
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
"precision": {
|
|
175
|
+
"type": "integer",
|
|
176
|
+
"minimum": 1,
|
|
177
|
+
"maximum": 12
|
|
178
|
+
},
|
|
179
|
+
"component": {
|
|
180
|
+
"enum": [
|
|
181
|
+
"w",
|
|
182
|
+
"s",
|
|
183
|
+
"e",
|
|
184
|
+
"n"
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
"segments": {
|
|
188
|
+
"description": "The typed member path the value is computed from: name segments for object members, index segments for array positions.",
|
|
189
|
+
"type": "array",
|
|
190
|
+
"items": {
|
|
191
|
+
"type": "object"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"required": [
|
|
196
|
+
"name",
|
|
197
|
+
"derive",
|
|
198
|
+
"segments"
|
|
199
|
+
],
|
|
200
|
+
"additionalProperties": false
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"note": {
|
|
204
|
+
"type": "string"
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"required": [
|
|
208
|
+
"kind",
|
|
209
|
+
"collection",
|
|
210
|
+
"columns"
|
|
211
|
+
],
|
|
212
|
+
"additionalProperties": false
|
|
142
213
|
}
|
|
143
214
|
}
|
|
144
215
|
}
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"$ref": "#/$defs/queryStep"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"$ref": "#/$defs/deriveStep"
|
|
58
61
|
}
|
|
59
62
|
]
|
|
60
63
|
},
|
|
@@ -139,6 +142,74 @@
|
|
|
139
142
|
"assert"
|
|
140
143
|
],
|
|
141
144
|
"additionalProperties": false
|
|
145
|
+
},
|
|
146
|
+
"deriveStep": {
|
|
147
|
+
"description": "Recompute named STORED derived index columns from the documents already in a collection. Only the physical mapping that cannot index a registered deterministic function has these columns; the generated mapping populates them itself. The step is idempotent — a derived value is a pure function of the document — so a replay writes what the first run wrote.",
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"kind": {
|
|
151
|
+
"const": "derive"
|
|
152
|
+
},
|
|
153
|
+
"collection": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
156
|
+
},
|
|
157
|
+
"columns": {
|
|
158
|
+
"description": "The columns to recompute, with the derivation and the typed member path each is computed from.",
|
|
159
|
+
"type": "array",
|
|
160
|
+
"minItems": 1,
|
|
161
|
+
"items": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"properties": {
|
|
164
|
+
"name": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"minLength": 1
|
|
167
|
+
},
|
|
168
|
+
"derive": {
|
|
169
|
+
"enum": [
|
|
170
|
+
"geohash",
|
|
171
|
+
"bbox"
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
"precision": {
|
|
175
|
+
"type": "integer",
|
|
176
|
+
"minimum": 1,
|
|
177
|
+
"maximum": 12
|
|
178
|
+
},
|
|
179
|
+
"component": {
|
|
180
|
+
"enum": [
|
|
181
|
+
"w",
|
|
182
|
+
"s",
|
|
183
|
+
"e",
|
|
184
|
+
"n"
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
"segments": {
|
|
188
|
+
"description": "The typed member path the value is computed from: name segments for object members, index segments for array positions.",
|
|
189
|
+
"type": "array",
|
|
190
|
+
"items": {
|
|
191
|
+
"type": "object"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"required": [
|
|
196
|
+
"name",
|
|
197
|
+
"derive",
|
|
198
|
+
"segments"
|
|
199
|
+
],
|
|
200
|
+
"additionalProperties": false
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"note": {
|
|
204
|
+
"type": "string"
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"required": [
|
|
208
|
+
"kind",
|
|
209
|
+
"collection",
|
|
210
|
+
"columns"
|
|
211
|
+
],
|
|
212
|
+
"additionalProperties": false
|
|
142
213
|
}
|
|
143
214
|
}
|
|
144
215
|
}
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"additionalProperties": false
|
|
81
81
|
},
|
|
82
82
|
"index": {
|
|
83
|
-
"description": "One index over a singular JSONPath expression (or a composite over several). A non-singular path is rejected at open (JD0004).",
|
|
83
|
+
"description": "One index over a singular JSONPath expression (or a composite over several). A non-singular path is rejected at open (JD0004). A derived index computes indexable scalars from the selected member instead of indexing it directly.",
|
|
84
84
|
"type": "object",
|
|
85
85
|
"properties": {
|
|
86
86
|
"name": {
|
|
@@ -108,6 +108,19 @@
|
|
|
108
108
|
"unique": {
|
|
109
109
|
"description": "Whether the index enforces uniqueness. Defaults to false.",
|
|
110
110
|
"type": "boolean"
|
|
111
|
+
},
|
|
112
|
+
"derive": {
|
|
113
|
+
"description": "Derive indexable columns from the selected spatial member instead of indexing it: 'geohash' (one TEXT cell column, precision required) or 'bbox' (four REAL columns — west, south, east, north). A derived index is never unique.",
|
|
114
|
+
"enum": [
|
|
115
|
+
"geohash",
|
|
116
|
+
"bbox"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"precision": {
|
|
120
|
+
"description": "Geohash cell length in characters, 1-12. Required beside derive: 'geohash' and refused anywhere else — there is no safe default, because the right cell size depends on the query radius, which the model cannot know.",
|
|
121
|
+
"type": "integer",
|
|
122
|
+
"minimum": 1,
|
|
123
|
+
"maximum": 12
|
|
111
124
|
}
|
|
112
125
|
},
|
|
113
126
|
"required": [
|