@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.
@@ -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: "an index path is not a singular member selection";
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` — an index path does not select exactly one member
66
- * (wildcards, slices, filters and descendants are not indexable);
67
- * the reason names the expression
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
@@ -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';
@@ -25,7 +25,10 @@
25
25
  * - **Shutdown is bounded.** Handlers receive an `AbortSignal` and
26
26
  * `stop()` takes a deadline, so a handler that never settles cannot
27
27
  * hold `stop()` — and therefore `store.close()`, and therefore the
28
- * database file — open forever.
28
+ * database file — open forever. A loop the deadline could not drain
29
+ * is CANCELLED, not merely left behind: when its handler finally
30
+ * settles it exits without another claim, store write or poll
31
+ * timer, and its abandoned job recovers by lease expiry (§5).
29
32
  */
30
33
  export declare const JOBS_TABLE = "_jaren_jobs";
31
34
  export declare const JOB_CHECKPOINTS_TABLE = "_jaren_job_checkpoints";
@@ -117,6 +120,9 @@ export declare function createJobEngine(options: {
117
120
  * the loops — but only up to `graceMs`. A handler that ignores its
118
121
  * signal cannot hold the process open; the resolved record says so
119
122
  * instead, and the lease expiry (§5) lets another worker re-claim.
123
+ * A loop the grace period could not drain is cancelled outright:
124
+ * when its handler finally settles it exits without another
125
+ * claim, store write or poll timer.
120
126
  * @param {{ graceMs?: number }} [stopOptions]
121
127
  * @returns {Promise<{ drained: boolean, inFlight: number }>}
122
128
  */
@@ -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 }} [options]
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: {
@@ -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
@@ -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
- * `{ $for: { it: '$[*]' }, $return: [ <ret> ] }` over the one-row
13
- * array; the array wrapper packs the item sequence so an
14
- * array-VALUED item stays unambiguous, and the per-row results
15
- * concatenate in row order (streamable).
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} returnExpression - The document's raw `$return` value
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(returnExpression: any, limits?: any, operators?: {
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[];
@@ -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;
@@ -125,7 +125,8 @@ stats() }`:
125
125
  plainly (§8);
126
126
  - `stop({ graceMs })` aborts in-flight handlers and resolves once they
127
127
  settle **or** the grace period expires (default 5 s), answering
128
- `{ drained, inFlight }` see §6.1;
128
+ `{ drained, inFlight }`; a loop the grace period could not drain is
129
+ **cancelled**, not left running — see §6.1;
129
130
  - `stats()` reports claims, completions, failures, wakes, polls and the
130
131
  in-flight handler count.
131
132
 
@@ -162,6 +163,16 @@ handle really is released. A handler that ignores its signal therefore
162
163
  cannot hold the database file open for the life of the process, and the
163
164
  lease expiry (§5) lets another worker re-claim its job.
164
165
 
166
+ A loop the grace period could not drain is **cancelled**, not merely
167
+ uncounted: when its wedged handler finally settles, the loop exits
168
+ without writing the completion or the failure, without claiming again,
169
+ and without re-arming its poll timer — the store it would touch is the
170
+ one the caller is closing, and the job it abandons recovers by lease
171
+ expiry (§5). A later `start()` builds fresh loops in a new session (and
172
+ re-registers the worker's wake-on-enqueue hook and its place in
173
+ `stopAll`), so a cancelled loop can never be revived as an extra
174
+ claimer.
175
+
165
176
  ## 7. The DAG composition
166
177
 
167
178
  ```js
@@ -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
@@ -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 `@jarenjs/validate`.
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` | an index path is not a singular member selection |
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.34.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.34.0",
75
- "@jarenjs/json": "^0.34.0",
76
- "@jarenjs/validate": "^0.34.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
  }