@sixb/sqlite 0.1.13 → 0.1.15

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.
Files changed (34) hide show
  1. package/dist/auth-storage/auth-storage.d.ts +2 -1
  2. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  3. package/dist/auth-storage/rows.d.ts +1 -1
  4. package/dist/auth-storage/rows.d.ts.map +1 -1
  5. package/dist/index.js +726 -286
  6. package/dist/index.js.map +12 -12
  7. package/dist/latest-run-query.d.ts +2 -0
  8. package/dist/latest-run-query.d.ts.map +1 -1
  9. package/dist/migrations.d.ts.map +1 -1
  10. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  11. package/dist/ontology-storage/shared.d.ts +11 -8
  12. package/dist/ontology-storage/shared.d.ts.map +1 -1
  13. package/dist/ontology-storage/source-cleanup.d.ts +7 -1
  14. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  15. package/dist/ontology-storage/source-roots.d.ts +14 -2
  16. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  17. package/dist/ontology-storage/sources.d.ts +8 -3
  18. package/dist/ontology-storage/sources.d.ts.map +1 -1
  19. package/dist/projection-run-storage.d.ts +3 -1
  20. package/dist/projection-run-storage.d.ts.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth-storage/auth-storage.ts +44 -79
  23. package/src/auth-storage/rows.ts +1 -1
  24. package/src/latest-run-query.ts +8 -2
  25. package/src/migrations/050-directory-group-memberships.sql +21 -0
  26. package/src/migrations/051-projection-run-supersession.sql +126 -0
  27. package/src/migrations/052-compact-source-storage.sql +181 -0
  28. package/src/migrations.ts +42 -0
  29. package/src/ontology-storage/materialization-state.ts +34 -44
  30. package/src/ontology-storage/shared.ts +20 -11
  31. package/src/ontology-storage/source-cleanup.ts +61 -24
  32. package/src/ontology-storage/source-roots.ts +166 -110
  33. package/src/ontology-storage/sources.ts +169 -118
  34. package/src/projection-run-storage.ts +59 -1
@@ -0,0 +1,126 @@
1
+ -- Projection runs gain the terminal status `superseded`, and a running run may carry the failure
2
+ -- of its last attempt in `error` while the queue retries it. SQLite cannot alter a CHECK
3
+ -- constraint, so the table is rebuilt.
4
+ CREATE TABLE projection_runs_v2 (
5
+ project_id TEXT NOT NULL,
6
+ id TEXT NOT NULL,
7
+ execution_id TEXT NOT NULL,
8
+ projection_id TEXT NOT NULL,
9
+ projection_kind TEXT NOT NULL CHECK (projection_kind IN ('object', 'link', 'telemetry')),
10
+ materialization_protocol TEXT NOT NULL CHECK (
11
+ materialization_protocol IN ('replacement', 'telemetry')
12
+ ),
13
+ dataset_id TEXT NOT NULL,
14
+ dataset_version_id TEXT NOT NULL,
15
+ dataset_version_created_at TEXT NOT NULL,
16
+ ontology_revision TEXT NOT NULL,
17
+ projection_revision TEXT NOT NULL,
18
+ ownership_hash TEXT NOT NULL,
19
+ object_type_id TEXT,
20
+ source_object_type_id TEXT,
21
+ target_object_type_id TEXT,
22
+ status TEXT NOT NULL CHECK (
23
+ status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled', 'superseded')
24
+ ),
25
+ queued_at TEXT NOT NULL,
26
+ started_at TEXT,
27
+ finished_at TEXT,
28
+ attempt INTEGER NOT NULL CHECK (attempt >= 0),
29
+ execution_token TEXT,
30
+ fixed_batch_size INTEGER CHECK (fixed_batch_size > 0),
31
+ next_batch_ordinal INTEGER CHECK (next_batch_ordinal >= 0),
32
+ next_row_offset INTEGER CHECK (next_row_offset >= 0),
33
+ input_exhausted INTEGER CHECK (input_exhausted IN (0, 1)),
34
+ missing_target_object_type_id TEXT,
35
+ missing_target_object_id TEXT,
36
+ missing_target_batch_ordinal INTEGER CHECK (
37
+ missing_target_batch_ordinal IS NULL OR missing_target_batch_ordinal >= 0
38
+ ),
39
+ missing_target_first_seen_at TEXT,
40
+ source_rows_read INTEGER NOT NULL DEFAULT 0 CHECK (source_rows_read >= 0),
41
+ source_rows_skipped INTEGER NOT NULL DEFAULT 0 CHECK (source_rows_skipped >= 0),
42
+ source_changes_read INTEGER CHECK (source_changes_read IS NULL OR source_changes_read >= 0),
43
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
44
+ PRIMARY KEY (project_id, id),
45
+ UNIQUE (project_id, execution_id),
46
+ FOREIGN KEY (project_id, execution_id) REFERENCES executions (project_id, id) ON DELETE RESTRICT,
47
+ CHECK (source_rows_skipped <= source_rows_read),
48
+ CHECK ((status = 'running') = (execution_token IS NOT NULL)),
49
+ CHECK (
50
+ (status = 'queued' AND attempt = 0 AND started_at IS NULL AND finished_at IS NULL
51
+ AND error IS NULL)
52
+ OR (status = 'running' AND attempt >= 1 AND started_at IS NOT NULL AND finished_at IS NULL)
53
+ OR (
54
+ status IN ('succeeded', 'failed', 'cancelled', 'superseded')
55
+ AND finished_at IS NOT NULL
56
+ AND (
57
+ (json_extract(error, '$.code') = 'queue.enqueue_failed'
58
+ AND status = 'failed' AND attempt = 0 AND started_at IS NULL)
59
+ OR (COALESCE(json_extract(error, '$.code'), '') != 'queue.enqueue_failed'
60
+ AND attempt >= 1 AND started_at IS NOT NULL)
61
+ )
62
+ )
63
+ ),
64
+ CHECK (status != 'succeeded' OR error IS NULL),
65
+ -- A run is superseded instead of materialized, so it carries no failure, and only replacement
66
+ -- runs can be: telemetry appends readings rather than replace a source.
67
+ CHECK (status != 'superseded' OR (error IS NULL AND materialization_protocol = 'replacement')),
68
+ CHECK ((projection_kind = 'telemetry') = (materialization_protocol = 'telemetry')),
69
+ CHECK (
70
+ (projection_kind = 'link' AND object_type_id IS NULL
71
+ AND source_object_type_id IS NOT NULL AND target_object_type_id IS NOT NULL)
72
+ OR (projection_kind IN ('object', 'telemetry') AND object_type_id IS NOT NULL
73
+ AND source_object_type_id IS NULL AND target_object_type_id IS NULL)
74
+ ),
75
+ CHECK (
76
+ (materialization_protocol = 'replacement' AND fixed_batch_size IS NULL
77
+ AND next_batch_ordinal IS NULL AND next_row_offset IS NULL AND input_exhausted IS NULL)
78
+ OR (materialization_protocol = 'telemetry' AND fixed_batch_size IS NOT NULL
79
+ AND next_batch_ordinal IS NOT NULL AND next_row_offset IS NOT NULL
80
+ AND input_exhausted IS NOT NULL)
81
+ ),
82
+ CHECK (
83
+ (missing_target_object_type_id IS NULL AND missing_target_object_id IS NULL
84
+ AND missing_target_batch_ordinal IS NULL AND missing_target_first_seen_at IS NULL)
85
+ OR (projection_kind = 'telemetry' AND missing_target_object_type_id = object_type_id
86
+ AND missing_target_object_id IS NOT NULL
87
+ AND missing_target_batch_ordinal = next_batch_ordinal
88
+ AND missing_target_first_seen_at IS NOT NULL)
89
+ ),
90
+ CHECK (projection_kind != 'telemetry' OR status != 'succeeded' OR input_exhausted = 1)
91
+ );
92
+
93
+ INSERT INTO projection_runs_v2 (
94
+ project_id, id, execution_id, projection_id, projection_kind, materialization_protocol,
95
+ dataset_id, dataset_version_id, dataset_version_created_at, ontology_revision,
96
+ projection_revision, ownership_hash, object_type_id, source_object_type_id,
97
+ target_object_type_id, status, queued_at, started_at, finished_at, attempt, execution_token,
98
+ fixed_batch_size, next_batch_ordinal, next_row_offset, input_exhausted,
99
+ missing_target_object_type_id, missing_target_object_id, missing_target_batch_ordinal,
100
+ missing_target_first_seen_at, source_rows_read, source_rows_skipped, source_changes_read, error
101
+ )
102
+ SELECT
103
+ project_id, id, execution_id, projection_id, projection_kind, materialization_protocol,
104
+ dataset_id, dataset_version_id, dataset_version_created_at, ontology_revision,
105
+ projection_revision, ownership_hash, object_type_id, source_object_type_id,
106
+ target_object_type_id, status, queued_at, started_at, finished_at, attempt, execution_token,
107
+ fixed_batch_size, next_batch_ordinal, next_row_offset, input_exhausted,
108
+ missing_target_object_type_id, missing_target_object_id, missing_target_batch_ordinal,
109
+ missing_target_first_seen_at, source_rows_read, source_rows_skipped, source_changes_read, error
110
+ FROM projection_runs;
111
+
112
+ DROP TABLE projection_runs;
113
+ ALTER TABLE projection_runs_v2 RENAME TO projection_runs;
114
+
115
+ CREATE INDEX idx_projection_runs_project_started
116
+ ON projection_runs(project_id, COALESCE(started_at, queued_at) DESC);
117
+ CREATE INDEX idx_projection_runs_project_projection_started
118
+ ON projection_runs(project_id, projection_id, COALESCE(started_at, queued_at) DESC);
119
+ CREATE INDEX idx_projection_runs_project_dataset_started
120
+ ON projection_runs(project_id, dataset_id, COALESCE(started_at, queued_at) DESC);
121
+ CREATE INDEX idx_projection_runs_project_version_started
122
+ ON projection_runs(project_id, dataset_version_id, COALESCE(started_at, queued_at) DESC);
123
+ CREATE INDEX idx_projection_runs_project_status_started
124
+ ON projection_runs(project_id, status, COALESCE(started_at, queued_at) DESC);
125
+ CREATE INDEX idx_projection_runs_project_object_type_started
126
+ ON projection_runs(project_id, object_type_id, COALESCE(started_at, queued_at) DESC);
@@ -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
@@ -112,6 +112,15 @@ import ontologyCommitAttributionSql from "./migrations/047-ontology-commit-attri
112
112
  type: "text",
113
113
  }
114
114
  import nativeSessionsSql from "./migrations/049-native-sessions.sql" with { type: "text" }
115
+ import directoryGroupMembershipsSql from "./migrations/050-directory-group-memberships.sql" with {
116
+ type: "text",
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
+ }
115
124
 
116
125
  const MIGRATIONS_TABLE_SQL = `
117
126
  CREATE TABLE IF NOT EXISTS sixb_migrations (
@@ -216,6 +225,27 @@ export const sqliteStorageMigrations = defineMigrations({
216
225
  sqliteSql("047-ontology-commit-attribution", ontologyCommitAttributionSql),
217
226
  sqliteSql("048-file-upload-sessions", fileUploadSessionsSql),
218
227
  sqliteSql("049-native-sessions", nativeSessionsSql),
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
+ ),
219
249
  ],
220
250
  })
221
251
 
@@ -374,6 +404,18 @@ function rowToMigrationRecord(row: unknown): MigrationRecord {
374
404
  }
375
405
  }
376
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
+
377
419
  function checksum(value: string): string {
378
420
  return createHash("sha256").update(value).digest("hex")
379
421
  }
@@ -43,8 +43,15 @@ import {
43
43
  parseJson,
44
44
  type SqliteOntologySourceAssertionRow,
45
45
  type SqliteStoredOverrideRow,
46
+ sourceAssertion,
46
47
  } from "./shared"
47
- import { activeSourceRows, replacementAssertionRows, replacementSourceRows } from "./source-roots"
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 rows.* FROM requested
448
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
442
449
  CROSS JOIN ontology_source_rows AS rows
443
- ON rows.project_id = ?
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
- JOIN ontology_source_roots AS sources
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
- JOIN ontology_source_roots AS sources
574
- ON sources.project_id = rows.project_id
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
- MIN(entity_sort_key), 1
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 ontology_source_rows AS rows
838
- JOIN ontology_source_roots AS sources
839
- ON sources.project_id = rows.project_id
840
- AND sources.source_id = rows.source_id
841
- AND sources.materialization_id = rows.materialization_id
842
- AND sources.root_sort_key = rows.root_sort_key
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 ontology_source_rows AS rows
852
- JOIN ontology_source_roots AS sources
853
- ON sources.project_id = rows.project_id
854
- AND sources.source_id = rows.source_id
855
- AND sources.materialization_id = rows.materialization_id
856
- AND sources.root_sort_key = rows.root_sort_key
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
- root: parseJson<StoredSourceAssertion["root"]>(row.root),
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)))`
@@ -1,7 +1,10 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import { parseSixbFailure } from "@sixb/core/internal/errors"
3
- import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
4
3
  import { MaterializationConflictError } from "@sixb/core/internal/materialization"
4
+ import {
5
+ sourceAssertionFromColumns,
6
+ sourceEntityFromKey,
7
+ } from "@sixb/core/internal/ontology-storage-provider"
5
8
  import type {
6
9
  AssertSourceMaterializationExecutionInput,
7
10
  OntologyCommitRecord,
@@ -48,6 +51,7 @@ export interface SqliteOntologyCommitRow {
48
51
  }
49
52
 
50
53
  export interface SqliteOntologySourceRow {
54
+ readonly version_id: number
51
55
  readonly project_id: string
52
56
  readonly source_id: string
53
57
  readonly materialization_id: string
@@ -74,19 +78,21 @@ export interface SqliteOntologySourceRow {
74
78
  readonly updated_at: string
75
79
  }
76
80
 
81
+ /** An assertion with its root and version, as `SOURCE_ASSERTION_COLUMNS` selects it. */
77
82
  export interface SqliteOntologySourceAssertionRow {
78
- readonly project_id: string
79
83
  readonly source_id: string
80
84
  readonly materialization_id: string
81
- readonly entity_kind: "object" | "link"
82
- readonly entity_key: string
83
- readonly entity_sort_key: string
84
- readonly root_kind: "object" | "link"
85
85
  readonly root_key: string
86
- readonly root_sort_key: string
87
86
  readonly staging_ordinal: number
88
- readonly root: string
89
- readonly assertion: string
87
+ readonly entity_kind: "object" | "link"
88
+ readonly object_type_id: string | null
89
+ readonly primary_id: string | null
90
+ readonly source_type_id: string | null
91
+ readonly source_primary_id: string | null
92
+ readonly link_id: string | null
93
+ readonly target_type_id: string | null
94
+ readonly target_primary_id: string | null
95
+ readonly payload: string | null
90
96
  }
91
97
 
92
98
  export interface SqliteStoredOverrideRow {
@@ -149,8 +155,11 @@ export function sourceRecord(row: SqliteOntologySourceRow): OntologySourceRecord
149
155
 
150
156
  export function sourceAssertion(row: SqliteOntologySourceAssertionRow): StageSourceAssertion {
151
157
  return {
152
- root: parseJson<ProjectionEntityRef>(row.root),
153
- assertion: parseJson<StageSourceAssertion["assertion"]>(row.assertion),
158
+ root: sourceEntityFromKey(row.root_key),
159
+ assertion: sourceAssertionFromColumns(
160
+ row,
161
+ row.payload === null ? null : parseJson<unknown>(row.payload)
162
+ ),
154
163
  stagingOrdinal: row.staging_ordinal,
155
164
  }
156
165
  }