@jarenjs/db 0.43.3 → 0.46.5

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.
@@ -9,8 +9,10 @@
9
9
  *
10
10
  * One plan shape covers this version: a guarded selection over ONE
11
11
  * collection with optional ordering, window, aggregate and a
12
- * whole-document projection. Constructs beyond it are residuals by
13
- * design (see ARCHITECTURE.md's deliberate-residual table).
12
+ * whole-document projection or, instead of an ordering and a window,
13
+ * a k-nearest RANK the engine finishes over the rows the plan fetches.
14
+ * Constructs beyond it are residuals by design (see ARCHITECTURE.md's
15
+ * deliberate-residual table).
14
16
  */
15
17
  /** The plan format version, carried on every plan. */
16
18
  export declare const PLAN_VERSION = 2;
@@ -83,6 +85,18 @@ export type PlanOrderTerm = {
83
85
  desc: boolean;
84
86
  emptyGreatest: boolean;
85
87
  };
88
+ export type PlanRank = {
89
+ column: string;
90
+ dims: number;
91
+ probe: {
92
+ lit: number[];
93
+ } | {
94
+ ext: string;
95
+ };
96
+ offset: number;
97
+ limit: number;
98
+ margin: number;
99
+ };
86
100
  export type Plan = {
87
101
  planVersion: number;
88
102
  alg: 'select';
@@ -93,6 +107,7 @@ export type Plan = {
93
107
  offset: number;
94
108
  limit: number | null;
95
109
  } | null;
110
+ rank: PlanRank | null;
96
111
  aggregate: {
97
112
  fn: 'count' | 'sum' | 'avg' | 'min' | 'max';
98
113
  ref: PlanRef | null;
@@ -137,6 +152,20 @@ export type Plan = {
137
152
  *
138
153
  * @typedef {{ ref: PlanRef, desc: boolean, emptyGreatest: boolean }} PlanOrderTerm
139
154
  *
155
+ * @typedef {{ column: string, dims: number,
156
+ * probe: { lit: number[] } | { ext: string },
157
+ * offset: number, limit: number, margin: number }} PlanRank
158
+ * The k-nearest stage: the packed vector column the ranking reads,
159
+ * its declared width, the probe (a plan-time literal vector, or the
160
+ * external that carries one at call time), the window the ENGINE
161
+ * will apply, and the inclusive score margin of the candidate cut.
162
+ * The column cuts — every row whose column score is within `margin`
163
+ * of the `offset + limit`-th best is a candidate — and the engine
164
+ * decides: the original document, its whole ordering and window
165
+ * included, runs over the candidates' documents. A plan carrying a
166
+ * rank carries no order and no window of its own: nothing in SQL
167
+ * orders or limits the fetch.
168
+ *
140
169
  * @typedef {{
141
170
  * planVersion: number,
142
171
  * alg: 'select',
@@ -144,6 +173,7 @@ export type Plan = {
144
173
  * filter: PlanPredicate | null,
145
174
  * order: PlanOrderTerm[] | null,
146
175
  * window: { offset: number, limit: number | null } | null,
176
+ * rank: PlanRank | null,
147
177
  * aggregate: { fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
148
178
  * ref: PlanRef | null } | null,
149
179
  * project: 'document',
@@ -56,25 +56,35 @@ export declare function schemaTypeAt(schema: any, segments: import('./dialect.js
56
56
  * is false): there the columns are ordinary ones the store writes. The
57
57
  * two mappings produce different declared text on purpose — a database
58
58
  * built under one and opened under the other really does disagree, and
59
- * `verifyShape` says so rather than papering over it.
59
+ * `verifyShape` says so rather than papering over it. A
60
+ * `derive: 'vector'` column is the stored shape under BOTH mappings
61
+ * (`derivedMappingFor`), so for it the two agree.
60
62
  * @param {string} name - The collection name (also the table name)
61
63
  * @param {{ schema: any, keySegments: { name: string }[] | null,
62
64
  * identity: string, indexes: { name: string, paths: string[],
63
65
  * unique: boolean, derive?: string | null, precision?: number,
64
- * docPath: string }[] }} collection - normalized
66
+ * dims?: number, docPath: string }[] }} collection - normalized
65
67
  * @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
68
+ * @param {{ derived?: 'virtual' | 'stored', rtree?: boolean }} [options]
69
+ * - `derived` is the physical mapping for derived columns:
70
+ * `'virtual'` (a generated column over a registered function) unless
71
+ * the driver says it cannot index one. `rtree` is whether the driver
72
+ * carries the R\*Tree module; when it does not, a column set that
73
+ * declared `physical: 'rtree'` falls back to the B-tree over its
74
+ * columns and `explain().prefilters[].via` reports which shape ran
75
+ * (MODEL-FORMAT §4) — a report, not a silent degradation
69
76
  * @returns {{
70
77
  * table: string, keyColumn: string, docColumn: string,
71
78
  * keyType: string,
72
79
  * generated: { name: string, type: string, pathText: string,
73
80
  * canonical: string }[],
74
81
  * derived: { name: string, derive: string, precision?: number,
75
- * component?: string, segments: any[] }[],
82
+ * component?: string, dims?: number, segments: any[] }[],
76
83
  * columnByCanonical: Map<string, string>,
77
84
  * createSql: string[],
85
+ * virtualTables: { stem: string, name: string, columns: string[],
86
+ * edges: string[], createSql: string, fillSql: string,
87
+ * triggers: { name: string, sql: string }[] }[],
78
88
  * expected: { columns: { name: string, type: string,
79
89
  * generated: boolean }[], indexes: { name: string, unique: boolean,
80
90
  * columns: string[] }[] },
@@ -92,10 +102,12 @@ export declare function planCollection(name: string, collection: {
92
102
  unique: boolean;
93
103
  derive?: string | null;
94
104
  precision?: number;
105
+ dims?: number;
95
106
  docPath: string;
96
107
  }[];
97
108
  }, dialect: any, options?: {
98
109
  derived?: 'virtual' | 'stored';
110
+ rtree?: boolean;
99
111
  }): {
100
112
  table: string;
101
113
  keyColumn: string;
@@ -112,10 +124,23 @@ export declare function planCollection(name: string, collection: {
112
124
  derive: string;
113
125
  precision?: number;
114
126
  component?: string;
127
+ dims?: number;
115
128
  segments: any[];
116
129
  }[];
117
130
  columnByCanonical: Map<string, string>;
118
131
  createSql: string[];
132
+ virtualTables: {
133
+ stem: string;
134
+ name: string;
135
+ columns: string[];
136
+ edges: string[];
137
+ createSql: string;
138
+ fillSql: string;
139
+ triggers: {
140
+ name: string;
141
+ sql: string;
142
+ }[];
143
+ }[];
119
144
  expected: {
120
145
  columns: {
121
146
  name: string;
@@ -2,20 +2,36 @@
2
2
  * @file Derived index columns: the one place a declared
3
3
  * `indexes[].derive` becomes a value. A spatial member is an array of
4
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.
5
+ * geohash cell or a bounding-box edge is what actually gets indexed;
6
+ * an embedding is an array of hundreds of numbers, and what gets
7
+ * stored is its packed, l2-normalized Float32 form.
6
8
  *
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.
9
+ * Every cell and every box comes from `@jarenjs/core/geo`, and every
10
+ * normalization and packing from `@jarenjs/core/vector`; nothing here
11
+ * computes arithmetic of its own. The spatial functions serve BOTH
12
+ * physical mappings registered as deterministic SQL functions inside
13
+ * a virtual generated column's expression where the driver can index
14
+ * them, and called directly on the write path where it cannot so
15
+ * the two branches cannot drift into different answers. The vector
16
+ * kind has ONE mapping, stored everywhere (`DERIVE_MAPPING`), and
17
+ * registers no function at all: a whole array re-derived per row as a
18
+ * host call is the cost the spatial work measured at 150×, a stored
19
+ * column is readable without any registration, and bun has no
20
+ * function API — one mapping is the only way every driver agrees.
13
21
  *
14
22
  * 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 neighbourhoodis computed by the
18
- * helpers below rather than by an import of its own.
23
+ * one home for spatial arithmetic, grep-proven by test) and onto
24
+ * `@jarenjs/core/vector` (the same rule, one home for vector
25
+ * arithmetic): the planner's probe geometry the box of a literal or
26
+ * bound region, the box of a bounded-distance circle, a cell's
27
+ * neighbourhood — and the k-nearest plan's probe vector and column
28
+ * score are computed by the helpers below rather than by an import of
29
+ * their own.
30
+ *
31
+ * The switches over the kind are EXHAUSTIVE: a kind with no rule
32
+ * throws, here and in the dialect, so that adding a kind without
33
+ * teaching both is a failure at the first call rather than a bbox
34
+ * edge of the first two floats and `jaren_bbox_undefined(...)` SQL.
19
35
  *
20
36
  * Determinism is the contract, not a convenience: a value here is a
21
37
  * pure function of the document bytes. Nothing reads the clock, a
@@ -28,6 +44,33 @@
28
44
  */
29
45
  /** The closed set of derive kinds. */
30
46
  export declare const DERIVE_KINDS: Set<string>;
47
+ /**
48
+ * The per-kind PHYSICAL MAPPING override. A spatial kind takes the
49
+ * mapping the driver's capability selects (`null` here); the vector
50
+ * kind is `'stored'` on every driver, for the three reasons in the
51
+ * file header, and never joins `registerDeriveFunctions`.
52
+ * @type {Readonly<Record<string, 'stored' | null>>}
53
+ */
54
+ export declare const DERIVE_MAPPING: Readonly<Record<string, 'stored' | null>>;
55
+ /**
56
+ * The physical mapping one derived column takes: the kind's override
57
+ * where it has one, the driver's mapping otherwise.
58
+ * @param {string} kind
59
+ * @param {'virtual' | 'stored'} driverMapping
60
+ * @returns {'virtual' | 'stored'}
61
+ */
62
+ export declare function derivedMappingFor(kind: string, driverMapping: 'virtual' | 'stored'): 'virtual' | 'stored';
63
+ /**
64
+ * The closed set of PHYSICAL realizations a `derive: 'bbox'` index may
65
+ * ask for. `'columns'` is the default and what an absent member means:
66
+ * four generated columns under one B-tree. `'rtree'` is the same four
67
+ * columns (they stay the box's one definition) beside a SQLite R\*Tree
68
+ * virtual table kept in sync by declared triggers, with no B-tree over
69
+ * them. The LOGICAL meaning of `derive: 'bbox'` is identical either
70
+ * way — same rows, same answers — which is the whole point of naming
71
+ * the shape separately from the derivation.
72
+ */
73
+ export declare const PHYSICAL_KINDS: Set<string>;
31
74
  /** A bbox index's four columns, in the order they are declared —
32
75
  * `[west, south, east, north]`, the order the kernel's boxes carry. */
33
76
  export declare const BBOX_COMPONENTS: readonly string[];
@@ -42,6 +85,9 @@ export declare const BBOX_INDEX_ORDER: readonly string[];
42
85
  /** The declared geohash precision range (characters). */
43
86
  export declare const PRECISION_MIN = 1;
44
87
  export declare const PRECISION_MAX = 12;
88
+ /** The declared vector width range (components). */
89
+ export declare const DIMS_MIN = 1;
90
+ export declare const DIMS_MAX = 8192;
45
91
  /**
46
92
  * The geohash cell of a value at a precision, or null when the value
47
93
  * carries no bounded position.
@@ -59,6 +105,20 @@ export declare function deriveGeohash(value: any, precision: number): string | n
59
105
  * @returns {number | null}
60
106
  */
61
107
  export declare function deriveBboxEdge(value: any, component: string): number | null;
108
+ /**
109
+ * The value of a `derive: 'vector'` column for a document member — the
110
+ * ONE seam from this package onto `@jarenjs/core/vector`. The member
111
+ * round-trips through its stored form first, exactly as the spatial
112
+ * kinds do: a `Float32Array` in the document is held as an object and
113
+ * a `NaN` as `null`, and the column must be a function of what is
114
+ * held, so that a write, a migration backfill and a second open can
115
+ * never disagree about one row.
116
+ * @param {any} member - the value at the index path, or `undefined`
117
+ * @param {number} dims - the declared width
118
+ * @returns {Uint8Array | null} `4·dims` bytes, or `null` when the
119
+ * member is not a vector of that width
120
+ */
121
+ export declare function deriveVector(member: any, dims: number): Uint8Array | null;
62
122
  /**
63
123
  * A member as the database will hold it. A derived value must be a
64
124
  * function of the STORED document, not of the object handed to the
@@ -75,16 +135,21 @@ export declare function deriveBboxEdge(value: any, component: string): number |
75
135
  */
76
136
  export declare function storedMemberForm(member: any): any;
77
137
  /**
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}
138
+ * The value of one derived column for a document member, which the
139
+ * caller hands over in its STORED form (`storedMemberForm`): the write
140
+ * path round-trips it, and a migration backfill reads it from the row.
141
+ * Exhaustive over the kind see the file header.
142
+ * @param {{ derive: string, precision?: number, component?: string,
143
+ * dims?: number }} column
144
+ * @param {any} member - the stored value at the index path, or `undefined`
145
+ * @returns {string | number | Uint8Array | null}
82
146
  */
83
147
  export declare function derivedValue(column: {
84
148
  derive: string;
85
149
  precision?: number;
86
150
  component?: string;
87
- }, member: any): string | number | null;
151
+ dims?: number;
152
+ }, member: any): string | number | Uint8Array | null;
88
153
  /**
89
154
  * Read the member one derived column is computed from, walking the
90
155
  * same typed segments the physical mapping was planned over.
@@ -159,3 +224,27 @@ export declare function derivedSlotValue(derived: {
159
224
  external: string;
160
225
  axis: string;
161
226
  }, value: any): number | null;
227
+ /**
228
+ * The probe a k-nearest plan scores the column against: a literal or
229
+ * bound vector, l2-normalized once, so that its dot product with the
230
+ * column's normalized form IS the cosine of the raw vectors. `null`
231
+ * when the value is not a vector of exactly `dims` finite numbers —
232
+ * the binder's signal to divert the call to the residual, where the
233
+ * engine answers what it answers everywhere for such a probe (empty
234
+ * keys for another width, its own refusal for a non-array).
235
+ * @param {unknown} value
236
+ * @param {number} dims - the column's declared width
237
+ * @returns {Float32Array | null}
238
+ */
239
+ export declare function probeVector(value: unknown, dims: number): Float32Array | null;
240
+ /**
241
+ * One row's column score against a prepared probe: the packed column
242
+ * unpacked at the declared width and dotted with the probe. `null` for
243
+ * a row whose column holds no vector — SQL `NULL`, or bytes of another
244
+ * length — because such a row is unrankable and 0 is a real score.
245
+ * @param {unknown} bytes - the column value as the driver returns it
246
+ * @param {number} dims
247
+ * @param {Float32Array} probe - from {@link probeVector}
248
+ * @returns {number | null}
249
+ */
250
+ export declare function columnScore(bytes: unknown, dims: number, probe: Float32Array): number | null;
@@ -36,6 +36,7 @@ export type JsonPathSegment = {
36
36
  * capabilities: Record<string, any>,
37
37
  * tableSuffix: string,
38
38
  * docColumnType: string,
39
+ * packedVectorType: string,
39
40
  * quoteIdentifier: (s: string) => string,
40
41
  * parameterRef: (i: number, name: string) => string,
41
42
  * stringLiteral: (s: string) => string,
@@ -45,7 +46,7 @@ export type JsonPathSegment = {
45
46
  * jsonPathText: (segments: JsonPathSegment[]) => string | null,
46
47
  * jsonExtract: (columnSql: string, pathText: string) => string,
47
48
  * derivedExpression?: (memberSql: string, column: { derive: string,
48
- * precision?: number, component?: string }) => string,
49
+ * precision?: number, component?: string, dims?: number }) => string,
49
50
  * jsonSet: (exprSql: string, pathText: string, valueSql: string) => string,
50
51
  * jsonRemove: (exprSql: string, pathText: string) => string,
51
52
  * jsonAppend: (exprSql: string, arrayPathText: string, valueSql: string) => string,
@@ -60,6 +61,8 @@ export type JsonPathSegment = {
60
61
  * strContains: (valueSql: string, patternSql: string) => string,
61
62
  * orderNulls: (nullsFirst: boolean) => string,
62
63
  * rowIdentity: () => string,
64
+ * identityIn: (identitySql: string, paramSqls: string[]) => string,
65
+ * rtree?: { module: string, columns: readonly string[] },
63
66
  * explainQuery: (sql: string) => string,
64
67
  * excludedRef: (columnSql: string) => string,
65
68
  * tx: { begin: string, beginImmediate: string, commit: string,
@@ -83,6 +86,7 @@ export declare function createDialect(spec: {
83
86
  capabilities: Record<string, any>;
84
87
  tableSuffix: string;
85
88
  docColumnType: string;
89
+ packedVectorType: string;
86
90
  quoteIdentifier: (s: string) => string;
87
91
  parameterRef: (i: number, name: string) => string;
88
92
  stringLiteral: (s: string) => string;
@@ -95,6 +99,7 @@ export declare function createDialect(spec: {
95
99
  derive: string;
96
100
  precision?: number;
97
101
  component?: string;
102
+ dims?: number;
98
103
  }) => string;
99
104
  jsonSet: (exprSql: string, pathText: string, valueSql: string) => string;
100
105
  jsonRemove: (exprSql: string, pathText: string) => string;
@@ -110,6 +115,11 @@ export declare function createDialect(spec: {
110
115
  strContains: (valueSql: string, patternSql: string) => string;
111
116
  orderNulls: (nullsFirst: boolean) => string;
112
117
  rowIdentity: () => string;
118
+ identityIn: (identitySql: string, paramSqls: string[]) => string;
119
+ rtree?: {
120
+ module: string;
121
+ columns: readonly string[];
122
+ };
113
123
  explainQuery: (sql: string) => string;
114
124
  excludedRef: (columnSql: string) => string;
115
125
  tx: {
@@ -18,7 +18,8 @@ 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
+ export { DERIVE_KINDS, DERIVE_MAPPING, PHYSICAL_KINDS, BBOX_COMPONENTS, BBOX_INDEX_ORDER, PRECISION_MIN, PRECISION_MAX, DIMS_MIN, DIMS_MAX, deriveGeohash, deriveBboxEdge, deriveVector, derivedValue, derivedMappingFor, memberAt, storedMemberForm, registerDeriveFunctions, probeVector, columnScore, } from './derive.js';
22
+ export { KNN_MARGIN, IDENTITY_CHUNK, cutCandidates, identityBatches } from './knn.js';
22
23
  export { createQueryEngine, createQueryState, createEntityQueryEngine, createLoadEngine, INCLUDE_DEPTH_DEFAULT, } from './query.js';
23
24
  export { normalizeProfile, SAFE_PROFILE, translateProfilePredicate, applyMandatoryPredicate, applyRowBound, } from './profile.js';
24
25
  export { translatePatch } from './patch-sql.js';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @file The candidate cut of a k-nearest plan: the arithmetic between
3
+ * the scores a statement fetched and the row identities the engine
4
+ * will decide over. No vector arithmetic lives here — a score is
5
+ * `derive.js`'s, through `@jarenjs/core/vector` — and no SQL: the
6
+ * fetch of the winners is the dialect's. This is the one place the
7
+ * margin is applied and the one place candidate identities are
8
+ * batched for it.
9
+ */
10
+ /**
11
+ * The inclusive score margin of the cut. The column's score is a dot
12
+ * product over binary32-normalized forms; the engine's key is the
13
+ * cosine of the raw doubles; measured, the two differ by at most
14
+ * ~1e-8. Any margin of at least twice that makes the engine's top
15
+ * `offset + limit` a SUBSET of the candidates: were a row the engine
16
+ * ranks inside the window cut, some candidate the engine ranks outside
17
+ * it would have to score higher by the column and lower by the engine,
18
+ * which two scores within half the margin of each other cannot do.
19
+ * This is a hundred times that bound — it admits, in practice, only
20
+ * true ties, and those the engine breaks by the document's own keys.
21
+ */
22
+ export declare const KNN_MARGIN = 0.000001;
23
+ /**
24
+ * The most identities one fetch statement binds: under the parameter
25
+ * cap of every SQLite build the store runs on. A guard, not a design —
26
+ * a k-nearest window is a handful of rows, and this only matters for a
27
+ * collection of many exact duplicates.
28
+ */
29
+ export declare const IDENTITY_CHUNK = 512;
30
+ /**
31
+ * The rows a k-nearest window can contain, from the scored fetch.
32
+ *
33
+ * `m = offset + limit` rows are needed. When at least `m` rows scored,
34
+ * the candidates are every scored row within `margin` of the m-th best
35
+ * score — ties at the boundary included by construction. When fewer
36
+ * did, the window reaches the unrankable tail (NULL columns, or a
37
+ * collection smaller than the window), which only the documents can
38
+ * order: every row is then a candidate, and the collection is no
39
+ * larger than the window.
40
+ * @param {{ identity: any, score: number | null }[]} rows - one per
41
+ * fetched row; `score` is `null` where the column held no vector
42
+ * @param {number} m - `offset + limit`
43
+ * @param {number} margin
44
+ * @returns {{ identities: any[], scored: number, full: boolean }} the
45
+ * candidate identities in ascending identity order — the
46
+ * collection's own order, which a stable sort over the candidates
47
+ * must see — with how many rows scored and whether every row was
48
+ * taken
49
+ */
50
+ export declare function cutCandidates(rows: {
51
+ identity: any;
52
+ score: number | null;
53
+ }[], m: number, margin: number): {
54
+ identities: any[];
55
+ scored: number;
56
+ full: boolean;
57
+ };
58
+ /**
59
+ * The identities sliced into fetch batches: at most `IDENTITY_CHUNK`
60
+ * each, and each padded with `null` to the next power of two — a NULL
61
+ * in an IN list matches no row — so a handful of prepared statements
62
+ * serve every candidate count instead of one per count seen.
63
+ * @param {any[]} identities - in the order they should be fetched
64
+ * @returns {{ size: number, params: any[] }[]}
65
+ */
66
+ export declare function identityBatches(identities: any[]): {
67
+ size: number;
68
+ params: any[];
69
+ }[];
@@ -50,7 +50,7 @@ export declare function migrationChecksum(migration: any): string;
50
50
  * @param {any} fromModel
51
51
  * @param {any} toModel
52
52
  * @param {{ id?: string, dialect?: any,
53
- * derived?: 'virtual' | 'stored' }} [options]
53
+ * derived?: 'virtual' | 'stored', rtree?: boolean }} [options]
54
54
  * @returns {{ migration: any, report: {
55
55
  * renamed: { from: string, to: string }[],
56
56
  * added: string[], removed: string[],
@@ -61,6 +61,7 @@ export declare function planMigration(fromModel: any, toModel: any, options?: {
61
61
  id?: string;
62
62
  dialect?: any;
63
63
  derived?: 'virtual' | 'stored';
64
+ rtree?: boolean;
64
65
  }): {
65
66
  migration: any;
66
67
  report: {
@@ -10,7 +10,7 @@
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' | 'knn', reasons, rowReturn,
14
14
  * prefilters }
15
15
  *
16
16
  * - `native` — everything translated; the plan alone answers.
@@ -21,6 +21,11 @@
21
21
  * wrapper built anywhere else would have to guess it.
22
22
  * - `set` — the pushed conjuncts narrow candidates; the WHOLE
23
23
  * compiled document runs over the materialized candidates.
24
+ * - `knn` — the pushed conjuncts narrow, the vector column CUTS the
25
+ * candidates of a k-nearest window (`plan.rank`), and the whole
26
+ * compiled document runs over the cut — a set residual whose
27
+ * candidate set an ordering, not a predicate, chose (see "The
28
+ * k-nearest promotion" below).
24
29
  *
25
30
  * `reasons` names every construct that forced work off the database,
26
31
  * with reason text drawn from the deliberate-residual table.
@@ -49,11 +54,12 @@ export declare function assertDecidedKind(node: any): void;
49
54
  * @returns {{
50
55
  * analysis: any,
51
56
  * plan: import('./algebra.js').Plan | null,
52
- * mode: 'native' | 'row' | 'set',
57
+ * mode: 'native' | 'row' | 'set' | 'knn',
53
58
  * reasons: { construct: string, reason: string }[],
54
59
  * rowReturn: any,
55
60
  * udfs: string[],
56
- * prefilters: { construct: string, columns: string[], exact: boolean }[],
61
+ * prefilters: { construct: string, via: 'columns' | 'rtree',
62
+ * columns: string[], exact: boolean }[],
57
63
  * }}
58
64
  */
59
65
  export declare function planQuery(document: any, shape: any, options?: {
@@ -64,7 +70,7 @@ export declare function planQuery(document: any, shape: any, options?: {
64
70
  }): {
65
71
  analysis: any;
66
72
  plan: import('./algebra.js').Plan | null;
67
- mode: 'native' | 'row' | 'set';
73
+ mode: 'native' | 'row' | 'set' | 'knn';
68
74
  reasons: {
69
75
  construct: string;
70
76
  reason: string;
@@ -73,6 +79,7 @@ export declare function planQuery(document: any, shape: any, options?: {
73
79
  udfs: string[];
74
80
  prefilters: {
75
81
  construct: string;
82
+ via: 'columns' | 'rtree';
76
83
  columns: string[];
77
84
  exact: boolean;
78
85
  }[];
@@ -17,6 +17,17 @@
17
17
  * residual over the full collection instead of the native statement —
18
18
  * SQLite cannot bind a boolean, a `null` needs Jaren's semantics, and
19
19
  * a missing external must raise the ENGINE's error, not a driver's.
20
+ * Two externals never bind at all and divert by their own rule: a
21
+ * region reaching the statement through derived slots diverts when it
22
+ * has no box, and a k-nearest probe — which the plan scores in the
23
+ * engine, never in SQL — diverts when it is not a vector of the
24
+ * column's width, so the engine answers what it answers everywhere.
25
+ *
26
+ * The k-nearest mode (`plan.rank`) is a set residual whose candidates
27
+ * an ordering chose: the statement fetches (identity, column) under
28
+ * the pushed WHERE, the engine scores and cuts (`knn.js`), the
29
+ * winners' documents are fetched by identity through the dialect, and
30
+ * the whole document runs over them.
20
31
  */
21
32
  /**
22
33
  * The store-wide query state shared by every collection's engine: one
@@ -172,9 +172,39 @@ what the pushdown planner already means by it.
172
172
  | `orderBy` over extractable paths, optional `limit`, offset 0 | **maintained window**: a sorted structure; ties broken by the collection key, appended as the final sort term; an insert sorting beyond a full window is a no-op | the window rows and their sort keys |
173
173
  | whole-query `count` / `sum` / `avg` / `min` / `max` (the plan's aggregate), optional `where` | **running accumulator** plus a per-row contribution map — a delete can only be answered from retained contributions (§3: a `remove` carries no old value). `min`/`max` removal of the last extremum holder FALLS BACK to a recompute over the retained contributions; the accumulator alone cannot answer, and this fallback is the documented cost | one contribution per matching row |
174
174
  | single-level `groupBy` with aggregate returns, in the canonical form below | **per-group deltas**: the accumulator machinery, one instance per group; groups appear in first-appearance order, exactly the engine's order | per-group, per-row contributions |
175
+ | `where` whose spatial predicate is **refined** (the plan pushed a bounding-box or cell-range pre-filter and left the exact `$within`, bounded `$distance` or over-long cell prefix to the residual — `explain().prefilters` with `exact: false`), no order, no aggregate; optional per-row `select` | **incremental rows** — the geofence: the initial fetch narrows through the derived index, and every touched row is re-evaluated by the engine's EXACT predicate, so a point emits `add` when it enters the region, `remove` when it leaves, and nothing while it moves within (a whole-document return sees a `replace` carrying the new position) | the result rows |
176
+ | a refined spatial predicate over a collection with **no document key** (`key: null`, rowid identity) | **re-run on invalidation** — the per-row strategy tracks a row by its declared key, and a rowid is not one; the reason says `rows without a document key cannot be tracked` | the previous result, for diffing |
177
+ | `orderBy` beside a refined spatial predicate — over `$distance` (not a path) or over a member (the set residual drops the planner's order terms) | **re-run on invalidation**, the ordering named as the reason | the previous result, for diffing |
178
+ | a whole-query aggregate or a `groupBy` whose `where` is a refined spatial predicate | **re-run on invalidation** — the accumulator needs a fully translated selection and a refinement is not one; the reason says so | the previous result, for diffing |
179
+ | a spatial predicate the planner **refused** (no `derive` index on the member, an untyped member, an unbounded probe) | **re-run on invalidation**, the refusal named — it never translated, so nothing narrows the fetch | the previous result, for diffing |
175
180
  | joins, multi-entity roots, graph loads, every entity query | **re-run on invalidation — declared, not attempted** in this version | the previous result, for diffing |
176
181
  | anything else: non-translatable predicates, `limit` without `orderBy`, `offset` > 0, windowed aggregates, `@jarenjs/linq`'s nested two-level `groupBy` emission, non-canonical group returns | **re-run on invalidation**, the reason named | the previous result, for diffing |
177
182
 
183
+ The **physical mapping** of a `derive: 'bbox'` index (MODEL-FORMAT §2.1,
184
+ `physical`) does not move a query between these rows. It can change one
185
+ input to the choice — `$bbox-intersects` translates exactly over four
186
+ generated columns and is refined over an R\*Tree, whose stored box is a
187
+ 32-bit-float superset — but both the exact row above and the refined one
188
+ below it are the maintained per-row strategy, so the geofence behaves
189
+ identically under either shape. That is checked, not assumed:
190
+ `test/db/geofence.test.js` runs the same watch under both mappings and
191
+ asserts the same strategy, the same emissions and the same rows.
192
+
193
+ **What the geofence costs, stated rather than discovered.** A refined
194
+ spatial predicate is maintained *per row*, not incrementally: there is
195
+ no live spatial index, and none is planned. Every insert or update the
196
+ collection sees runs the exact predicate — `$within` against the
197
+ region, a geodesic `$distance` — once for that row, inside capture
198
+ delivery, on the store's own connection. Over a large region (a ring of
199
+ thousands of vertices) at a high write rate that is real work on every
200
+ write, and a consumer with such a region either simplifies it for the
201
+ fence (`$geo-simplify`) or accepts the cost knowingly. The region is
202
+ bound at registration like every external (§8): the initial fetch binds
203
+ it through the same derived parameter slots the planner uses for a
204
+ one-off query, so an external region narrows exactly as a literal one
205
+ does, and a region with no bounding box diverts to a full initial read
206
+ and is still correct.
207
+
178
208
  Re-run is a first-class, documented outcome, not a failure. What is
179
209
  forbidden is *silently* re-running while the reader believes the query
180
210
  is incremental: `live.mode` reports `'incremental'` or `'rerun'`, the
@@ -84,6 +84,16 @@ A `jslt` transform on such a collection gets the same treatment for the
84
84
  same reason: it rewrites the documents the columns are computed from,
85
85
  so the planner follows it with a `derive` step that recomputes them.
86
86
 
87
+ A `derive: 'vector'` column (MODEL-FORMAT §2.1) is stored under BOTH
88
+ mappings, so the planner emits its backfill whatever `derived` says,
89
+ and the column entry carries the width the value is packed to:
90
+
91
+ ```json
92
+ { "kind": "derive", "collection": "items",
93
+ "columns": [ { "name": "gx_embedding_v768", "derive": "vector", "dims": 768,
94
+ "segments": [ { "name": "embedding" } ] } ] }
95
+ ```
96
+
87
97
  ## 3. Planning and the widening/narrowing rule
88
98
 
89
99
  `planMigration(fromModel, toModel, { dialect, id, derived })` produces