@sixb/sqlite 0.1.14 → 0.1.16
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/dist/index.js +739 -261
- package/dist/index.js.map +14 -14
- package/dist/latest-run-query.d.ts +2 -0
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/query-compiler.d.ts +3 -0
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +2 -0
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +1 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/rows.d.ts +1 -0
- package/dist/objects/rows.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +11 -8
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts +7 -1
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
- package/dist/ontology-storage/source-roots.d.ts +14 -2
- package/dist/ontology-storage/source-roots.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +8 -3
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/projection-run-storage.d.ts +3 -1
- package/dist/projection-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/latest-run-query.ts +8 -2
- package/src/migrations/051-projection-run-supersession.sql +126 -0
- package/src/migrations/052-compact-source-storage.sql +181 -0
- package/src/migrations.ts +38 -0
- package/src/objects/README.md +19 -0
- package/src/objects/query-compiler.ts +44 -7
- package/src/objects/read-scope.ts +10 -8
- package/src/objects/reader.ts +43 -8
- package/src/objects/rows.ts +1 -0
- package/src/ontology-storage/materialization-state.ts +34 -44
- package/src/ontology-storage/shared.ts +20 -11
- package/src/ontology-storage/source-cleanup.ts +61 -24
- package/src/ontology-storage/source-roots.ts +166 -110
- package/src/ontology-storage/sources.ts +169 -118
- package/src/projection-run-storage.ts +59 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
-- Source versions get a compact surrogate key; roots and rows stop repeating the text triple.
|
|
2
|
+
-- SQLite cannot add a key column to a table, so the three source tables are rebuilt. Index
|
|
3
|
+
-- names are schema-wide: the old ones are dropped first to free them.
|
|
4
|
+
DROP INDEX idx_ontology_sources_active;
|
|
5
|
+
DROP INDEX idx_ontology_sources_run_candidate;
|
|
6
|
+
DROP INDEX idx_ontology_sources_cleanup;
|
|
7
|
+
DROP INDEX idx_ontology_sources_run;
|
|
8
|
+
DROP INDEX idx_ontology_source_roots_active;
|
|
9
|
+
DROP INDEX idx_ontology_source_roots_lookup;
|
|
10
|
+
DROP INDEX idx_ontology_source_roots_cleanup;
|
|
11
|
+
DROP INDEX idx_ontology_source_rows_root;
|
|
12
|
+
DROP INDEX idx_ontology_source_rows_staging_ordinal;
|
|
13
|
+
DROP INDEX idx_ontology_source_rows_entity_sort;
|
|
14
|
+
DROP INDEX idx_ontology_source_rows_object;
|
|
15
|
+
DROP INDEX idx_ontology_source_rows_link_source;
|
|
16
|
+
DROP INDEX idx_ontology_source_rows_link_target;
|
|
17
|
+
ALTER TABLE ontology_source_rows RENAME TO legacy_ontology_source_rows;
|
|
18
|
+
ALTER TABLE ontology_source_roots RENAME TO legacy_ontology_source_roots;
|
|
19
|
+
ALTER TABLE ontology_sources RENAME TO legacy_ontology_sources;
|
|
20
|
+
|
|
21
|
+
CREATE TABLE ontology_sources (
|
|
22
|
+
version_id INTEGER PRIMARY KEY,
|
|
23
|
+
project_id TEXT NOT NULL,
|
|
24
|
+
source_id TEXT NOT NULL,
|
|
25
|
+
materialization_id TEXT NOT NULL,
|
|
26
|
+
projection_run_id TEXT NOT NULL,
|
|
27
|
+
projection_kind TEXT NOT NULL CHECK (projection_kind IN ('object', 'link')),
|
|
28
|
+
protocol TEXT NOT NULL CHECK (protocol = 'replacement'),
|
|
29
|
+
status TEXT NOT NULL CHECK (
|
|
30
|
+
status IN ('staging', 'ready', 'active', 'superseded', 'abandoned')
|
|
31
|
+
),
|
|
32
|
+
execution_token TEXT,
|
|
33
|
+
dataset_id TEXT NOT NULL,
|
|
34
|
+
dataset_version_id TEXT NOT NULL,
|
|
35
|
+
dataset_version_created_at TEXT NOT NULL,
|
|
36
|
+
projection_revision TEXT NOT NULL,
|
|
37
|
+
ownership_hash TEXT NOT NULL,
|
|
38
|
+
ontology_revision TEXT NOT NULL,
|
|
39
|
+
root_count INTEGER CHECK (root_count IS NULL OR root_count >= 0),
|
|
40
|
+
assertion_count INTEGER CHECK (assertion_count IS NULL OR assertion_count >= 0),
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
ready_at TEXT,
|
|
43
|
+
activated_at TEXT,
|
|
44
|
+
terminal_at TEXT,
|
|
45
|
+
last_commit_id TEXT,
|
|
46
|
+
updated_at TEXT NOT NULL,
|
|
47
|
+
base_materialization_id TEXT,
|
|
48
|
+
base_commit_id TEXT,
|
|
49
|
+
UNIQUE (project_id, source_id, materialization_id),
|
|
50
|
+
CHECK ((base_materialization_id IS NULL) = (base_commit_id IS NULL)),
|
|
51
|
+
CHECK ((root_count IS NULL) = (assertion_count IS NULL)),
|
|
52
|
+
CHECK ((root_count IS NULL) = (ready_at IS NULL)),
|
|
53
|
+
CHECK ((status IN ('staging', 'ready')) = (execution_token IS NOT NULL)),
|
|
54
|
+
CHECK (
|
|
55
|
+
(status = 'staging' AND ready_at IS NULL)
|
|
56
|
+
OR (status IN ('ready', 'active', 'superseded') AND ready_at IS NOT NULL)
|
|
57
|
+
OR status = 'abandoned'
|
|
58
|
+
),
|
|
59
|
+
CHECK ((status IN ('active', 'superseded')) = (activated_at IS NOT NULL)),
|
|
60
|
+
CHECK ((status IN ('active', 'superseded')) = (last_commit_id IS NOT NULL)),
|
|
61
|
+
CHECK ((status IN ('superseded', 'abandoned')) = (terminal_at IS NOT NULL)),
|
|
62
|
+
CHECK (ready_at IS NULL OR created_at <= ready_at),
|
|
63
|
+
CHECK (activated_at IS NULL OR (ready_at IS NOT NULL AND ready_at <= activated_at)),
|
|
64
|
+
CHECK (terminal_at IS NULL OR created_at <= terminal_at),
|
|
65
|
+
CHECK (terminal_at IS NULL OR ready_at IS NULL OR ready_at <= terminal_at),
|
|
66
|
+
CHECK (terminal_at IS NULL OR activated_at IS NULL OR activated_at <= terminal_at),
|
|
67
|
+
CHECK (created_at <= updated_at)
|
|
68
|
+
);
|
|
69
|
+
INSERT INTO ontology_sources (
|
|
70
|
+
project_id, source_id, materialization_id, projection_run_id, projection_kind, protocol,
|
|
71
|
+
status, execution_token, dataset_id, dataset_version_id, dataset_version_created_at,
|
|
72
|
+
projection_revision, ownership_hash, ontology_revision, root_count, assertion_count,
|
|
73
|
+
created_at, ready_at, activated_at, terminal_at, last_commit_id, updated_at,
|
|
74
|
+
base_materialization_id, base_commit_id
|
|
75
|
+
)
|
|
76
|
+
SELECT project_id, source_id, materialization_id, projection_run_id, projection_kind, protocol,
|
|
77
|
+
status, execution_token, dataset_id, dataset_version_id, dataset_version_created_at,
|
|
78
|
+
projection_revision, ownership_hash, ontology_revision, root_count, assertion_count,
|
|
79
|
+
created_at, ready_at, activated_at, terminal_at, last_commit_id, updated_at,
|
|
80
|
+
base_materialization_id, base_commit_id
|
|
81
|
+
FROM legacy_ontology_sources
|
|
82
|
+
ORDER BY rowid;
|
|
83
|
+
CREATE UNIQUE INDEX idx_ontology_sources_active
|
|
84
|
+
ON ontology_sources(project_id, source_id)
|
|
85
|
+
WHERE status = 'active';
|
|
86
|
+
CREATE UNIQUE INDEX idx_ontology_sources_run_candidate
|
|
87
|
+
ON ontology_sources(project_id, projection_run_id)
|
|
88
|
+
WHERE status IN ('staging', 'ready');
|
|
89
|
+
CREATE INDEX idx_ontology_sources_cleanup
|
|
90
|
+
ON ontology_sources(project_id, status, terminal_at, source_id, materialization_id);
|
|
91
|
+
CREATE INDEX idx_ontology_sources_run
|
|
92
|
+
ON ontology_sources(project_id, projection_run_id);
|
|
93
|
+
|
|
94
|
+
-- A root is live while its version is published (active or superseded), it is not retired and
|
|
95
|
+
-- it is not a deletion. Activation therefore writes only the roots it replaces, never its own.
|
|
96
|
+
-- `root_key` is the canonical key; SQLite's default BINARY collation orders it by its UTF-8 bytes.
|
|
97
|
+
CREATE TABLE ontology_source_roots (
|
|
98
|
+
id INTEGER PRIMARY KEY,
|
|
99
|
+
version_id INTEGER NOT NULL REFERENCES ontology_sources (version_id) ON DELETE RESTRICT,
|
|
100
|
+
project_id TEXT NOT NULL,
|
|
101
|
+
root_key TEXT NOT NULL,
|
|
102
|
+
staging_ordinal INTEGER NOT NULL CHECK (staging_ordinal >= 0),
|
|
103
|
+
deleted INTEGER NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
|
|
104
|
+
retired_at TEXT,
|
|
105
|
+
CONSTRAINT ontology_source_roots_key UNIQUE (version_id, root_key),
|
|
106
|
+
CONSTRAINT ontology_source_roots_ordinal UNIQUE (version_id, staging_ordinal)
|
|
107
|
+
);
|
|
108
|
+
CREATE INDEX idx_ontology_source_roots_live
|
|
109
|
+
ON ontology_source_roots (project_id, root_key) WHERE retired_at IS NULL AND deleted = 0;
|
|
110
|
+
CREATE INDEX idx_ontology_source_roots_retired
|
|
111
|
+
ON ontology_source_roots (project_id, retired_at) WHERE retired_at IS NOT NULL;
|
|
112
|
+
|
|
113
|
+
-- A row's identity lives in its typed columns; `payload` is the assertion without kind and ref.
|
|
114
|
+
CREATE TABLE ontology_source_rows (
|
|
115
|
+
root_id INTEGER NOT NULL REFERENCES ontology_source_roots (id) ON DELETE RESTRICT,
|
|
116
|
+
entity_kind TEXT NOT NULL CHECK (entity_kind IN ('object', 'link')),
|
|
117
|
+
object_type_id TEXT,
|
|
118
|
+
primary_id TEXT,
|
|
119
|
+
source_type_id TEXT,
|
|
120
|
+
source_primary_id TEXT,
|
|
121
|
+
link_id TEXT,
|
|
122
|
+
target_type_id TEXT,
|
|
123
|
+
target_primary_id TEXT,
|
|
124
|
+
payload TEXT CHECK (json_valid(payload)),
|
|
125
|
+
CHECK (
|
|
126
|
+
(
|
|
127
|
+
entity_kind = 'object'
|
|
128
|
+
AND object_type_id IS NOT NULL AND primary_id IS NOT NULL
|
|
129
|
+
AND source_type_id IS NULL AND source_primary_id IS NULL AND link_id IS NULL
|
|
130
|
+
AND target_type_id IS NULL AND target_primary_id IS NULL
|
|
131
|
+
)
|
|
132
|
+
OR
|
|
133
|
+
(
|
|
134
|
+
entity_kind = 'link'
|
|
135
|
+
AND object_type_id IS NULL AND primary_id IS NULL
|
|
136
|
+
AND source_type_id IS NOT NULL AND source_primary_id IS NOT NULL AND link_id IS NOT NULL
|
|
137
|
+
AND target_type_id IS NOT NULL AND target_primary_id IS NOT NULL
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
CREATE INDEX idx_ontology_source_rows_root ON ontology_source_rows (root_id);
|
|
142
|
+
CREATE INDEX idx_ontology_source_rows_link_source
|
|
143
|
+
ON ontology_source_rows (source_type_id, source_primary_id, link_id)
|
|
144
|
+
WHERE entity_kind = 'link';
|
|
145
|
+
CREATE INDEX idx_ontology_source_rows_link_target
|
|
146
|
+
ON ontology_source_rows (target_type_id, target_primary_id)
|
|
147
|
+
WHERE entity_kind = 'link';
|
|
148
|
+
|
|
149
|
+
-- A published root that was not active is retired, so liveness stays exactly what it was.
|
|
150
|
+
INSERT INTO ontology_source_roots (
|
|
151
|
+
version_id, project_id, root_key, staging_ordinal, deleted, retired_at
|
|
152
|
+
)
|
|
153
|
+
SELECT versions.version_id, roots.project_id, roots.root_key, roots.staging_ordinal,
|
|
154
|
+
roots.deleted,
|
|
155
|
+
CASE
|
|
156
|
+
WHEN roots.active = 1 THEN NULL
|
|
157
|
+
WHEN roots.retired_at IS NOT NULL THEN roots.retired_at
|
|
158
|
+
WHEN versions.status IN ('active', 'superseded') THEN versions.updated_at
|
|
159
|
+
END
|
|
160
|
+
FROM legacy_ontology_source_roots AS roots
|
|
161
|
+
JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id)
|
|
162
|
+
ORDER BY versions.version_id, roots.staging_ordinal;
|
|
163
|
+
|
|
164
|
+
INSERT INTO ontology_source_rows (
|
|
165
|
+
root_id, entity_kind, object_type_id, primary_id,
|
|
166
|
+
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
|
|
167
|
+
)
|
|
168
|
+
SELECT roots.id, rows.entity_kind, rows.object_type_id, rows.primary_id,
|
|
169
|
+
rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
|
|
170
|
+
rows.target_primary_id, NULLIF(json_remove(rows.assertion, '$.kind', '$.ref'), '{}')
|
|
171
|
+
FROM legacy_ontology_source_rows AS rows
|
|
172
|
+
JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id)
|
|
173
|
+
JOIN ontology_source_roots AS roots
|
|
174
|
+
ON roots.version_id = versions.version_id AND roots.root_key = rows.root_key
|
|
175
|
+
ORDER BY roots.id;
|
|
176
|
+
|
|
177
|
+
DROP TABLE legacy_ontology_source_rows;
|
|
178
|
+
DROP TABLE legacy_ontology_source_roots;
|
|
179
|
+
DROP TABLE legacy_ontology_sources;
|
|
180
|
+
-- No ANALYZE: nothing refreshes SQLite statistics afterwards, and on a fresh install they would
|
|
181
|
+
-- describe empty tables forever and steer the planner to partial indexes over primary keys.
|
package/src/migrations.ts
CHANGED
|
@@ -115,6 +115,12 @@ import nativeSessionsSql from "./migrations/049-native-sessions.sql" with { type
|
|
|
115
115
|
import directoryGroupMembershipsSql from "./migrations/050-directory-group-memberships.sql" with {
|
|
116
116
|
type: "text",
|
|
117
117
|
}
|
|
118
|
+
import projectionRunSupersessionSql from "./migrations/051-projection-run-supersession.sql" with {
|
|
119
|
+
type: "text",
|
|
120
|
+
}
|
|
121
|
+
import compactSourceStorageSql from "./migrations/052-compact-source-storage.sql" with {
|
|
122
|
+
type: "text",
|
|
123
|
+
}
|
|
118
124
|
|
|
119
125
|
const MIGRATIONS_TABLE_SQL = `
|
|
120
126
|
CREATE TABLE IF NOT EXISTS sixb_migrations (
|
|
@@ -220,6 +226,26 @@ export const sqliteStorageMigrations = defineMigrations({
|
|
|
220
226
|
sqliteSql("048-file-upload-sessions", fileUploadSessionsSql),
|
|
221
227
|
sqliteSql("049-native-sessions", nativeSessionsSql),
|
|
222
228
|
sqliteSql("050-directory-group-memberships", directoryGroupMembershipsSql),
|
|
229
|
+
sqliteSql("051-projection-run-supersession", projectionRunSupersessionSql),
|
|
230
|
+
sqliteStep(
|
|
231
|
+
"052-compact-source-storage",
|
|
232
|
+
(db) => {
|
|
233
|
+
// SQLite has no procedural block to check the copy in SQL: count around it instead.
|
|
234
|
+
const before = sourceStorageCounts(db)
|
|
235
|
+
db.run(compactSourceStorageSql)
|
|
236
|
+
const after = sourceStorageCounts(db)
|
|
237
|
+
if (
|
|
238
|
+
after.versions !== before.versions ||
|
|
239
|
+
after.roots !== before.roots ||
|
|
240
|
+
after.rows !== before.rows
|
|
241
|
+
) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"[SixbSqliteStorage] Source versions, roots or rows were lost while compacting source storage."
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{ checksum: checksum(compactSourceStorageSql) }
|
|
248
|
+
),
|
|
223
249
|
],
|
|
224
250
|
})
|
|
225
251
|
|
|
@@ -378,6 +404,18 @@ function rowToMigrationRecord(row: unknown): MigrationRecord {
|
|
|
378
404
|
}
|
|
379
405
|
}
|
|
380
406
|
|
|
407
|
+
function sourceStorageCounts(db: Database): {
|
|
408
|
+
readonly versions: number
|
|
409
|
+
readonly roots: number
|
|
410
|
+
readonly rows: number
|
|
411
|
+
} {
|
|
412
|
+
return db
|
|
413
|
+
.query(`SELECT (SELECT count(*) FROM ontology_sources) AS versions,
|
|
414
|
+
(SELECT count(*) FROM ontology_source_roots) AS roots,
|
|
415
|
+
(SELECT count(*) FROM ontology_source_rows) AS rows`)
|
|
416
|
+
.get() as { readonly versions: number; readonly roots: number; readonly rows: number }
|
|
417
|
+
}
|
|
418
|
+
|
|
381
419
|
function checksum(value: string): string {
|
|
382
420
|
return createHash("sha256").update(value).digest("hex")
|
|
383
421
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Object reads
|
|
2
|
+
|
|
3
|
+
`read-scope.ts` derives visible objects, links and property grants from the selected roots and
|
|
4
|
+
paths. The selected reader holds its traversal-budget probe and terminal statements on one WAL
|
|
5
|
+
snapshot. SQL must filter by those relations before ranking or limiting results.
|
|
6
|
+
|
|
7
|
+
SQLite flattens CTEs. Expose identity columns from the grant/visible relation and put authorized
|
|
8
|
+
parents before indexed lookups with `CROSS JOIN`; exposing stored identities can cause a full
|
|
9
|
+
grant scan per traversed edge. The query-plan regression test checks those lookups.
|
|
10
|
+
|
|
11
|
+
Vector `list()` materializes at most the candidate budget plus one identity/vector pair per
|
|
12
|
+
eligible object. Its count controls distance evaluation and travels with the ranked rows, so
|
|
13
|
+
admission, total and ranking share one scan. Returned objects are hydrated after top-k; candidate
|
|
14
|
+
filters and traversals may still evaluate masked properties while deciding eligibility.
|
|
15
|
+
Overflow fails before exposing rows; it never silently ranks a truncated candidate set.
|
|
16
|
+
|
|
17
|
+
An outer zero limit can suppress the count row; that case uses the separate bounded probe.
|
|
18
|
+
Count, existence and facet terminals also retain that probe. Totals count the top-k set before
|
|
19
|
+
the outer limit, and all candidates must grant every profile source property.
|
|
@@ -24,7 +24,14 @@ export interface SqliteObjectQueryPageRow {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface CompiledObjectQuery {
|
|
27
|
-
vectorProbe?: {
|
|
27
|
+
vectorProbe?: {
|
|
28
|
+
sql: string
|
|
29
|
+
args: SqliteValue[]
|
|
30
|
+
limit: number
|
|
31
|
+
/** Maximum returned rows and total before the outermost limit, respectively. */
|
|
32
|
+
rowLimit: number
|
|
33
|
+
totalLimit: number
|
|
34
|
+
}
|
|
28
35
|
sql: string
|
|
29
36
|
args: SqliteValue[]
|
|
30
37
|
totalSql: string
|
|
@@ -98,7 +105,7 @@ export function compileObjectQuery(
|
|
|
98
105
|
if (hasVectorProfile(query) && !isVectorProfileQuery(query)) {
|
|
99
106
|
throw new ObjectQueryExecutionError(
|
|
100
107
|
"unsupported_vector_composition",
|
|
101
|
-
"Vector search supports one profile with filters before ranking and limit/project after."
|
|
108
|
+
"Vector search supports one profile with filters, traversals or sets before ranking and limit/project after."
|
|
102
109
|
)
|
|
103
110
|
}
|
|
104
111
|
const source = options.source ?? DEFAULT_OBJECT_QUERY_SOURCE
|
|
@@ -231,9 +238,31 @@ function compileVector(
|
|
|
231
238
|
{ kind: "column", column: "_vector_score", direction: "desc" },
|
|
232
239
|
...identityOrderFields().map((field) => ({ ...field, binary: true })),
|
|
233
240
|
])
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
const rankedOrder = compileOrder(order.fields, "ranked")
|
|
242
|
+
// Materialize only bounded, authorized candidates once. CASE prevents distance evaluation
|
|
243
|
+
// above the envelope; the reader rejects the count before exposing any rows or scores.
|
|
244
|
+
const sql = `SELECT stored.project_id, stored.object_type_id, stored.primary_id,
|
|
245
|
+
stored.properties, stored.properties AS _cursor_properties,
|
|
246
|
+
stored.created_at, stored.updated_at, stored.version, stored.last_commit_id,
|
|
247
|
+
ranked._vector_candidate_count, ranked._vector_score
|
|
248
|
+
FROM (
|
|
249
|
+
WITH _sixb_vector_candidates AS MATERIALIZED (
|
|
250
|
+
SELECT input.project_id, input.object_type_id, input.primary_id,
|
|
251
|
+
vectors.embedding AS _vector_embedding ${candidates} LIMIT ?
|
|
252
|
+
), _sixb_vector_budget AS (
|
|
253
|
+
SELECT COUNT(*) AS _vector_candidate_count FROM _sixb_vector_candidates
|
|
254
|
+
)
|
|
255
|
+
SELECT input.project_id, input.object_type_id, input.primary_id, budget._vector_candidate_count,
|
|
256
|
+
CASE WHEN budget._vector_candidate_count <= ?
|
|
257
|
+
THEN max(-1, min(1, 1 - vec_distance_cosine(input._vector_embedding, ?)))
|
|
258
|
+
END AS _vector_score
|
|
259
|
+
FROM _sixb_vector_candidates AS input CROSS JOIN _sixb_vector_budget AS budget
|
|
260
|
+
ORDER BY ${order.sql} LIMIT ?
|
|
261
|
+
) AS ranked CROSS JOIN ${ctx.source.objectsTable} AS stored
|
|
262
|
+
ON stored.project_id = ranked.project_id AND stored.object_type_id = ranked.object_type_id
|
|
263
|
+
AND stored.primary_id = ranked.primary_id
|
|
264
|
+
ORDER BY ${rankedOrder.sql}`
|
|
265
|
+
const args = [...candidateArgs, limit + 1, limit, encodeSqliteVector(values), query.k]
|
|
237
266
|
return {
|
|
238
267
|
sql,
|
|
239
268
|
args,
|
|
@@ -245,6 +274,8 @@ function compileVector(
|
|
|
245
274
|
sql: `SELECT COUNT(*) AS total FROM (SELECT 1 ${candidates} LIMIT ?) AS candidates`,
|
|
246
275
|
args: [...candidateArgs, limit + 1],
|
|
247
276
|
limit,
|
|
277
|
+
rowLimit: query.k,
|
|
278
|
+
totalLimit: query.k,
|
|
248
279
|
},
|
|
249
280
|
hasMore: () => false,
|
|
250
281
|
trimRows: identityRows,
|
|
@@ -451,7 +482,13 @@ function compileLimit(
|
|
|
451
482
|
FROM (${input.sql}) AS input
|
|
452
483
|
`,
|
|
453
484
|
totalArgs: input.args,
|
|
454
|
-
vectorProbe: input.vectorProbe
|
|
485
|
+
vectorProbe: input.vectorProbe
|
|
486
|
+
? {
|
|
487
|
+
...input.vectorProbe,
|
|
488
|
+
rowLimit: Math.min(input.vectorProbe.rowLimit, rowLimit),
|
|
489
|
+
totalLimit: input.vectorProbe.rowLimit,
|
|
490
|
+
}
|
|
491
|
+
: undefined,
|
|
455
492
|
order: input.order,
|
|
456
493
|
hasMore: (rowCount, total) =>
|
|
457
494
|
total === undefined ? ctx.probeLimit && rowCount > limit : limit < total,
|
|
@@ -852,7 +889,7 @@ function compileProject(
|
|
|
852
889
|
input.created_at,
|
|
853
890
|
input.updated_at,
|
|
854
891
|
input.version,
|
|
855
|
-
input.last_commit_id${input.vectorProbe ? ", input._vector_score" : ""}
|
|
892
|
+
input.last_commit_id${input.vectorProbe ? ", input._vector_score, input._vector_candidate_count" : ""}
|
|
856
893
|
FROM (${input.sql}) AS input
|
|
857
894
|
ORDER BY ${inputOrder.sql}
|
|
858
895
|
`,
|
|
@@ -22,6 +22,8 @@ export interface SqliteSelectedObjectReadSource extends SqliteObjectQuerySource
|
|
|
22
22
|
* CROSS JOIN keeps authorized parents/grants before indexed edge/object lookups, even when
|
|
23
23
|
* statistics would otherwise make SQLite rescan all grants for every stored object.
|
|
24
24
|
* Property grants are grouped only after path-sensitive reachability has been resolved.
|
|
25
|
+
* Expose identities from grants/visible, so flattened joins can seek those relations by key
|
|
26
|
+
* before loading stored values. Exposing stored identities makes traversal rescan grants per edge.
|
|
25
27
|
*/
|
|
26
28
|
export function compileSqliteSelectedObjectReadSource(
|
|
27
29
|
projectId: string,
|
|
@@ -105,7 +107,7 @@ function compileSelectedReadScopeCte(scopeJson: string, projectId: string): Comp
|
|
|
105
107
|
_sixb_scope_root_facts(project_id, root_id, node_id, object_type_id, primary_id) AS (
|
|
106
108
|
SELECT anchor.project_id, root.root_id, root.node_id, root.object_type_id, root.primary_id
|
|
107
109
|
FROM _sixb_scope_roots AS root
|
|
108
|
-
JOIN objects AS anchor
|
|
110
|
+
CROSS JOIN objects AS anchor
|
|
109
111
|
ON anchor.project_id = ?
|
|
110
112
|
AND anchor.object_type_id = root.object_type_id
|
|
111
113
|
AND anchor.primary_id = root.primary_id
|
|
@@ -187,7 +189,7 @@ function compileSelectedReadScopeCte(scopeJson: string, projectId: string): Comp
|
|
|
187
189
|
GROUP BY reachable.project_id, reachable.object_type_id, reachable.primary_id
|
|
188
190
|
),
|
|
189
191
|
_sixb_scope_objects AS (
|
|
190
|
-
SELECT
|
|
192
|
+
SELECT grants.project_id, grants.object_type_id, grants.primary_id,
|
|
191
193
|
COALESCE((
|
|
192
194
|
SELECT json_group_object(property.key, ${sqliteJsonEachValue("property")})
|
|
193
195
|
FROM json_each(stored.properties) AS property
|
|
@@ -277,12 +279,12 @@ function compileSelectedReadScopeCte(scopeJson: string, projectId: string): Comp
|
|
|
277
279
|
),
|
|
278
280
|
_sixb_scope_links AS (
|
|
279
281
|
SELECT
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
visible.project_id,
|
|
283
|
+
visible.source_type_id,
|
|
284
|
+
visible.source_id,
|
|
285
|
+
visible.link_id,
|
|
286
|
+
visible.target_type_id,
|
|
287
|
+
visible.target_id,
|
|
286
288
|
CASE
|
|
287
289
|
WHEN stored.properties IS NULL THEN NULL
|
|
288
290
|
ELSE NULLIF(
|
package/src/objects/reader.ts
CHANGED
|
@@ -54,23 +54,23 @@ export class SqliteObjectReader {
|
|
|
54
54
|
ensureSqliteVectorSearch(this.db)
|
|
55
55
|
const probe = compiled.vectorProbe
|
|
56
56
|
const row = this.db.query<{ total: number }, SqliteValue[]>(probe.sql).get(...probe.args)
|
|
57
|
-
|
|
58
|
-
throw new ObjectQueryExecutionError(
|
|
59
|
-
"vector_work_limit_exceeded",
|
|
60
|
-
`Exact vector search supports at most ${probe.limit} eligible vectors at this dimension. Narrow the query filters.`
|
|
61
|
-
)
|
|
62
|
-
}
|
|
57
|
+
assertVectorCandidateCount(row?.total, probe.limit)
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
queryObjects(params: QueryObjectsInput): QueryObjectsResult {
|
|
66
|
-
this.assertVectorSearch(params)
|
|
67
61
|
const source = this.source
|
|
68
62
|
const compiled = compileObjectQuery(params.projectId, params.query, {
|
|
69
63
|
includeTotal: params.includeTotal,
|
|
70
64
|
source,
|
|
71
65
|
})
|
|
72
|
-
|
|
66
|
+
if (compiled.vectorProbe) ensureSqliteVectorSearch(this.db)
|
|
67
|
+
const ordinaryTotal =
|
|
68
|
+
params.includeTotal === false || compiled.vectorProbe
|
|
69
|
+
? undefined
|
|
70
|
+
: readTotal(this.db, compiled)
|
|
73
71
|
const rawRows = this.db.query(compiled.sql).all(...compiled.args) as ObjectQueryDatabaseRow[]
|
|
72
|
+
const vectorTotal = this.vectorResultTotal(compiled, rawRows)
|
|
73
|
+
const total = params.includeTotal === false ? undefined : (vectorTotal ?? ordinaryTotal)
|
|
74
74
|
const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
|
|
75
75
|
const hasMore =
|
|
76
76
|
total === undefined && compiled.hasMoreProbe
|
|
@@ -87,6 +87,26 @@ export class SqliteObjectReader {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
private vectorResultTotal(
|
|
91
|
+
compiled: CompiledObjectQuery,
|
|
92
|
+
rows: readonly ObjectQueryDatabaseRow[]
|
|
93
|
+
): number | undefined {
|
|
94
|
+
const probe = compiled.vectorProbe
|
|
95
|
+
if (!probe) return undefined
|
|
96
|
+
|
|
97
|
+
// A zero outer limit can hide the count row. Keep admission and total semantics for that
|
|
98
|
+
// uncommon case, without rescanning candidates for ordinary vector queries.
|
|
99
|
+
let count = rows[0]?._vector_candidate_count
|
|
100
|
+
if (rows.length === 0) {
|
|
101
|
+
count =
|
|
102
|
+
probe.rowLimit === 0
|
|
103
|
+
? this.db.query<{ total: number }, SqliteValue[]>(probe.sql).get(...probe.args)?.total
|
|
104
|
+
: 0
|
|
105
|
+
}
|
|
106
|
+
assertVectorCandidateCount(count, probe.limit)
|
|
107
|
+
return Math.min(count, probe.totalLimit)
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
countObjects(params: CountObjectsInput): CountObjectsResult {
|
|
91
111
|
this.assertVectorSearch(params)
|
|
92
112
|
const source = this.source
|
|
@@ -519,6 +539,21 @@ function sqliteFacetValue(valueType: string | null, value: unknown): unknown {
|
|
|
519
539
|
return value
|
|
520
540
|
}
|
|
521
541
|
|
|
542
|
+
function assertVectorCandidateCount(count: unknown, limit: number): asserts count is number {
|
|
543
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
|
|
544
|
+
throw new ObjectQueryExecutionError(
|
|
545
|
+
"invalid_vector_candidate_count",
|
|
546
|
+
"Storage did not return a valid vector candidate count."
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
if (count > limit) {
|
|
550
|
+
throw new ObjectQueryExecutionError(
|
|
551
|
+
"vector_work_limit_exceeded",
|
|
552
|
+
`Exact vector search supports at most ${limit} eligible vectors at this dimension. Narrow the query filters.`
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
522
557
|
function existsProbeQuery(query: ObjectQuery): ObjectQuery {
|
|
523
558
|
return { kind: "limit", limit: 1, input: stripOuterRowShape(query) }
|
|
524
559
|
}
|
package/src/objects/rows.ts
CHANGED
|
@@ -107,6 +107,7 @@ export interface DatabaseRow {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export interface ObjectQueryDatabaseRow extends DatabaseRow {
|
|
110
|
+
_vector_candidate_count?: number
|
|
110
111
|
_vector_score?: number
|
|
111
112
|
_cursor_properties?: string
|
|
112
113
|
/** `json_object(linkId, value, ...)` serialized text from an `expand` pushdown; absent otherwise. */
|
|
@@ -43,8 +43,15 @@ import {
|
|
|
43
43
|
parseJson,
|
|
44
44
|
type SqliteOntologySourceAssertionRow,
|
|
45
45
|
type SqliteStoredOverrideRow,
|
|
46
|
+
sourceAssertion,
|
|
46
47
|
} from "./shared"
|
|
47
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
activeSourceRows,
|
|
50
|
+
LIVE_SOURCE_ROWS_JOIN,
|
|
51
|
+
replacementAssertionRows,
|
|
52
|
+
replacementSourceRows,
|
|
53
|
+
SOURCE_ASSERTION_COLUMNS,
|
|
54
|
+
} from "./source-roots"
|
|
48
55
|
|
|
49
56
|
export const SQLITE_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work"
|
|
50
57
|
export const SQLITE_REPLACEMENT_WORK_TABLE = "ontology_replacement_work"
|
|
@@ -438,19 +445,13 @@ export class SqliteMaterializationStateReader {
|
|
|
438
445
|
json_extract(value, '$.linkId') AS link_id
|
|
439
446
|
FROM json_each(?)
|
|
440
447
|
)
|
|
441
|
-
SELECT
|
|
448
|
+
SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
|
|
442
449
|
CROSS JOIN ontology_source_rows AS rows
|
|
443
|
-
ON rows.
|
|
444
|
-
AND rows.entity_kind = 'link'
|
|
450
|
+
ON rows.entity_kind = 'link'
|
|
445
451
|
AND rows.source_type_id = requested.source_type_id
|
|
446
452
|
AND rows.source_primary_id = requested.source_primary_id
|
|
447
453
|
AND rows.link_id = requested.link_id
|
|
448
|
-
|
|
449
|
-
ON sources.project_id = rows.project_id
|
|
450
|
-
AND sources.source_id = rows.source_id
|
|
451
|
-
AND sources.materialization_id = rows.materialization_id
|
|
452
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
453
|
-
WHERE sources.active = 1`
|
|
454
|
+
${LIVE_SOURCE_ROWS_JOIN}`
|
|
454
455
|
)
|
|
455
456
|
.all(requestedJson, this.projectId) as SqliteOntologySourceAssertionRow[]
|
|
456
457
|
const slotOverrideRows = this.db
|
|
@@ -570,12 +571,8 @@ export class SqliteMaterializationStateReader {
|
|
|
570
571
|
SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
|
|
571
572
|
rows.target_type_id, rows.target_primary_id
|
|
572
573
|
FROM ontology_source_rows AS rows
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
AND sources.source_id = rows.source_id
|
|
576
|
-
AND sources.materialization_id = rows.materialization_id
|
|
577
|
-
AND sources.root_sort_key = rows.root_sort_key
|
|
578
|
-
WHERE rows.project_id = ? AND rows.entity_kind = 'link' AND sources.active = 1
|
|
574
|
+
${LIVE_SOURCE_ROWS_JOIN}
|
|
575
|
+
WHERE rows.entity_kind = 'link'
|
|
579
576
|
), selected AS (
|
|
580
577
|
SELECT DISTINCT links.*,
|
|
581
578
|
${linkSortExpression("links")} AS sort_key
|
|
@@ -752,7 +749,7 @@ export class SqliteMaterializationStateReader {
|
|
|
752
749
|
}
|
|
753
750
|
|
|
754
751
|
private prepareReplacementObjects(input: ReplacementIdentityInput): void {
|
|
755
|
-
const rows = replacementSourceRows({
|
|
752
|
+
const rows = replacementSourceRows(this.db, {
|
|
756
753
|
projectId: this.projectId,
|
|
757
754
|
sourceId: input.sourceId,
|
|
758
755
|
materializationId: input.candidateMaterializationId,
|
|
@@ -765,7 +762,7 @@ export class SqliteMaterializationStateReader {
|
|
|
765
762
|
session_id, entity_kind, identity_key, sort_key, diff_required
|
|
766
763
|
)
|
|
767
764
|
SELECT ?, 'object', json_array(object_type_id, primary_id),
|
|
768
|
-
|
|
765
|
+
${objectSortExpression()}, 1
|
|
769
766
|
FROM (${rows.sql}) AS rows
|
|
770
767
|
WHERE entity_kind = 'object'
|
|
771
768
|
GROUP BY object_type_id, primary_id
|
|
@@ -775,7 +772,7 @@ export class SqliteMaterializationStateReader {
|
|
|
775
772
|
}
|
|
776
773
|
|
|
777
774
|
private prepareReplacementLinks(input: ReplacementIdentityInput): void {
|
|
778
|
-
const rows = replacementSourceRows({
|
|
775
|
+
const rows = replacementSourceRows(this.db, {
|
|
779
776
|
projectId: this.projectId,
|
|
780
777
|
sourceId: input.sourceId,
|
|
781
778
|
materializationId: input.candidateMaterializationId,
|
|
@@ -834,31 +831,21 @@ export class SqliteMaterializationStateReader {
|
|
|
834
831
|
UNION
|
|
835
832
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
836
833
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
837
|
-
FROM
|
|
838
|
-
JOIN
|
|
839
|
-
ON
|
|
840
|
-
AND
|
|
841
|
-
AND
|
|
842
|
-
|
|
843
|
-
JOIN incident_objects
|
|
844
|
-
ON incident_objects.object_type_id = rows.source_type_id
|
|
845
|
-
AND incident_objects.primary_id = rows.source_primary_id
|
|
846
|
-
WHERE rows.project_id = ? AND rows.entity_kind = 'link'
|
|
847
|
-
AND sources.active = 1
|
|
834
|
+
FROM incident_objects
|
|
835
|
+
CROSS JOIN ontology_source_rows AS rows
|
|
836
|
+
ON rows.entity_kind = 'link'
|
|
837
|
+
AND rows.source_type_id = incident_objects.object_type_id
|
|
838
|
+
AND rows.source_primary_id = incident_objects.primary_id
|
|
839
|
+
${LIVE_SOURCE_ROWS_JOIN}
|
|
848
840
|
UNION
|
|
849
841
|
SELECT rows.source_type_id, rows.source_primary_id AS source_id,
|
|
850
842
|
rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
|
|
851
|
-
FROM
|
|
852
|
-
JOIN
|
|
853
|
-
ON
|
|
854
|
-
AND
|
|
855
|
-
AND
|
|
856
|
-
|
|
857
|
-
JOIN incident_objects
|
|
858
|
-
ON incident_objects.object_type_id = rows.target_type_id
|
|
859
|
-
AND incident_objects.primary_id = rows.target_primary_id
|
|
860
|
-
WHERE rows.project_id = ? AND rows.entity_kind = 'link'
|
|
861
|
-
AND sources.active = 1
|
|
843
|
+
FROM incident_objects
|
|
844
|
+
CROSS JOIN ontology_source_rows AS rows
|
|
845
|
+
ON rows.entity_kind = 'link'
|
|
846
|
+
AND rows.target_type_id = incident_objects.object_type_id
|
|
847
|
+
AND rows.target_primary_id = incident_objects.primary_id
|
|
848
|
+
${LIVE_SOURCE_ROWS_JOIN}
|
|
862
849
|
), diff_links AS (
|
|
863
850
|
SELECT * FROM replacement_links
|
|
864
851
|
UNION SELECT * FROM incident_links
|
|
@@ -1062,9 +1049,7 @@ function storedSource(row: SqliteOntologySourceAssertionRow): StoredSourceAssert
|
|
|
1062
1049
|
return {
|
|
1063
1050
|
source: { projectionId: row.source_id },
|
|
1064
1051
|
materializationId: row.materialization_id,
|
|
1065
|
-
|
|
1066
|
-
assertion: parseJson<StoredSourceAssertion["assertion"]>(row.assertion),
|
|
1067
|
-
stagingOrdinal: row.staging_ordinal,
|
|
1052
|
+
...sourceAssertion(row),
|
|
1068
1053
|
} as StoredSourceAssertion
|
|
1069
1054
|
}
|
|
1070
1055
|
|
|
@@ -1130,6 +1115,11 @@ function storedPoint(row: TelemetryRow): StoredTelemetryPoint {
|
|
|
1130
1115
|
}
|
|
1131
1116
|
}
|
|
1132
1117
|
|
|
1118
|
+
function objectSortExpression(alias?: string): string {
|
|
1119
|
+
const prefix = alias ? `${alias}.` : ""
|
|
1120
|
+
return `LOWER(HEX(CAST(json_array(${prefix}object_type_id, ${prefix}primary_id) AS BLOB)))`
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1133
1123
|
export function linkSortExpression(alias?: string): string {
|
|
1134
1124
|
const prefix = alias ? `${alias}.` : ""
|
|
1135
1125
|
return `LOWER(HEX(CAST(json_array(${prefix}source_type_id, ${prefix}source_id, ${prefix}link_id, ${prefix}target_type_id, ${prefix}target_id) AS BLOB)))`
|