@jarenjs/db 0.43.1 → 0.46.4
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 +157 -13
- package/README.md +319 -33
- package/dist/types/algebra.d.ts +32 -2
- package/dist/types/ddl.d.ts +31 -6
- package/dist/types/derive.d.ts +105 -16
- package/dist/types/dialect.d.ts +11 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/knn.d.ts +69 -0
- package/dist/types/migrate.d.ts +2 -1
- package/dist/types/plan.d.ts +11 -4
- package/dist/types/query.d.ts +11 -0
- package/docs/LIVE-FORMAT.md +30 -0
- package/docs/MIGRATION-FORMAT.md +10 -0
- package/docs/MODEL-FORMAT.md +298 -27
- package/package.json +4 -4
- package/schemas/jaren-migration.draft-07.schema.json +8 -2
- package/schemas/jaren-migration.schema.json +8 -2
- package/schemas/jaren-model.draft-07.schema.json +16 -2
- package/schemas/jaren-model.schema.json +16 -2
- package/src/algebra.js +20 -2
- package/src/ddl.js +234 -26
- package/src/derive.js +158 -19
- package/src/dialect.js +134 -2
- package/src/dialects/sqlite.js +33 -4
- package/src/emit.js +30 -6
- package/src/index.js +5 -3
- package/src/knn.js +96 -0
- package/src/live.js +87 -7
- package/src/migrate.js +55 -4
- package/src/plan.js +280 -36
- package/src/query.js +177 -44
- package/src/store.js +151 -60
package/dist/types/algebra.d.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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',
|
package/dist/types/ddl.d.ts
CHANGED
|
@@ -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]
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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;
|
package/dist/types/derive.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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)
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
-
|
|
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;
|
package/dist/types/dialect.d.ts
CHANGED
|
@@ -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: {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
+
}[];
|
package/dist/types/migrate.d.ts
CHANGED
|
@@ -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: {
|
package/dist/types/plan.d.ts
CHANGED
|
@@ -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,
|
|
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
|
}[];
|
package/dist/types/query.d.ts
CHANGED
|
@@ -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
|
package/docs/LIVE-FORMAT.md
CHANGED
|
@@ -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
|
package/docs/MIGRATION-FORMAT.md
CHANGED
|
@@ -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
|