@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.
Files changed (40) hide show
  1. package/dist/index.js +739 -261
  2. package/dist/index.js.map +14 -14
  3. package/dist/latest-run-query.d.ts +2 -0
  4. package/dist/latest-run-query.d.ts.map +1 -1
  5. package/dist/migrations.d.ts.map +1 -1
  6. package/dist/objects/query-compiler.d.ts +3 -0
  7. package/dist/objects/query-compiler.d.ts.map +1 -1
  8. package/dist/objects/read-scope.d.ts +2 -0
  9. package/dist/objects/read-scope.d.ts.map +1 -1
  10. package/dist/objects/reader.d.ts +1 -0
  11. package/dist/objects/reader.d.ts.map +1 -1
  12. package/dist/objects/rows.d.ts +1 -0
  13. package/dist/objects/rows.d.ts.map +1 -1
  14. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  15. package/dist/ontology-storage/shared.d.ts +11 -8
  16. package/dist/ontology-storage/shared.d.ts.map +1 -1
  17. package/dist/ontology-storage/source-cleanup.d.ts +7 -1
  18. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  19. package/dist/ontology-storage/source-roots.d.ts +14 -2
  20. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  21. package/dist/ontology-storage/sources.d.ts +8 -3
  22. package/dist/ontology-storage/sources.d.ts.map +1 -1
  23. package/dist/projection-run-storage.d.ts +3 -1
  24. package/dist/projection-run-storage.d.ts.map +1 -1
  25. package/package.json +3 -3
  26. package/src/latest-run-query.ts +8 -2
  27. package/src/migrations/051-projection-run-supersession.sql +126 -0
  28. package/src/migrations/052-compact-source-storage.sql +181 -0
  29. package/src/migrations.ts +38 -0
  30. package/src/objects/README.md +19 -0
  31. package/src/objects/query-compiler.ts +44 -7
  32. package/src/objects/read-scope.ts +10 -8
  33. package/src/objects/reader.ts +43 -8
  34. package/src/objects/rows.ts +1 -0
  35. package/src/ontology-storage/materialization-state.ts +34 -44
  36. package/src/ontology-storage/shared.ts +20 -11
  37. package/src/ontology-storage/source-cleanup.ts +61 -24
  38. package/src/ontology-storage/source-roots.ts +166 -110
  39. package/src/ontology-storage/sources.ts +169 -118
  40. package/src/projection-run-storage.ts +59 -1
package/dist/index.js CHANGED
@@ -4357,6 +4357,319 @@ CREATE INDEX idx_auth_group_memberships_group
4357
4357
  ON auth_group_memberships(project_id, group_id);
4358
4358
  `;
4359
4359
 
4360
+ // src/migrations/051-projection-run-supersession.sql
4361
+ var _051_projection_run_supersession_default = `-- Projection runs gain the terminal status \`superseded\`, and a running run may carry the failure
4362
+ -- of its last attempt in \`error\` while the queue retries it. SQLite cannot alter a CHECK
4363
+ -- constraint, so the table is rebuilt.
4364
+ CREATE TABLE projection_runs_v2 (
4365
+ project_id TEXT NOT NULL,
4366
+ id TEXT NOT NULL,
4367
+ execution_id TEXT NOT NULL,
4368
+ projection_id TEXT NOT NULL,
4369
+ projection_kind TEXT NOT NULL CHECK (projection_kind IN ('object', 'link', 'telemetry')),
4370
+ materialization_protocol TEXT NOT NULL CHECK (
4371
+ materialization_protocol IN ('replacement', 'telemetry')
4372
+ ),
4373
+ dataset_id TEXT NOT NULL,
4374
+ dataset_version_id TEXT NOT NULL,
4375
+ dataset_version_created_at TEXT NOT NULL,
4376
+ ontology_revision TEXT NOT NULL,
4377
+ projection_revision TEXT NOT NULL,
4378
+ ownership_hash TEXT NOT NULL,
4379
+ object_type_id TEXT,
4380
+ source_object_type_id TEXT,
4381
+ target_object_type_id TEXT,
4382
+ status TEXT NOT NULL CHECK (
4383
+ status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled', 'superseded')
4384
+ ),
4385
+ queued_at TEXT NOT NULL,
4386
+ started_at TEXT,
4387
+ finished_at TEXT,
4388
+ attempt INTEGER NOT NULL CHECK (attempt >= 0),
4389
+ execution_token TEXT,
4390
+ fixed_batch_size INTEGER CHECK (fixed_batch_size > 0),
4391
+ next_batch_ordinal INTEGER CHECK (next_batch_ordinal >= 0),
4392
+ next_row_offset INTEGER CHECK (next_row_offset >= 0),
4393
+ input_exhausted INTEGER CHECK (input_exhausted IN (0, 1)),
4394
+ missing_target_object_type_id TEXT,
4395
+ missing_target_object_id TEXT,
4396
+ missing_target_batch_ordinal INTEGER CHECK (
4397
+ missing_target_batch_ordinal IS NULL OR missing_target_batch_ordinal >= 0
4398
+ ),
4399
+ missing_target_first_seen_at TEXT,
4400
+ source_rows_read INTEGER NOT NULL DEFAULT 0 CHECK (source_rows_read >= 0),
4401
+ source_rows_skipped INTEGER NOT NULL DEFAULT 0 CHECK (source_rows_skipped >= 0),
4402
+ source_changes_read INTEGER CHECK (source_changes_read IS NULL OR source_changes_read >= 0),
4403
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
4404
+ PRIMARY KEY (project_id, id),
4405
+ UNIQUE (project_id, execution_id),
4406
+ FOREIGN KEY (project_id, execution_id) REFERENCES executions (project_id, id) ON DELETE RESTRICT,
4407
+ CHECK (source_rows_skipped <= source_rows_read),
4408
+ CHECK ((status = 'running') = (execution_token IS NOT NULL)),
4409
+ CHECK (
4410
+ (status = 'queued' AND attempt = 0 AND started_at IS NULL AND finished_at IS NULL
4411
+ AND error IS NULL)
4412
+ OR (status = 'running' AND attempt >= 1 AND started_at IS NOT NULL AND finished_at IS NULL)
4413
+ OR (
4414
+ status IN ('succeeded', 'failed', 'cancelled', 'superseded')
4415
+ AND finished_at IS NOT NULL
4416
+ AND (
4417
+ (json_extract(error, '$.code') = 'queue.enqueue_failed'
4418
+ AND status = 'failed' AND attempt = 0 AND started_at IS NULL)
4419
+ OR (COALESCE(json_extract(error, '$.code'), '') != 'queue.enqueue_failed'
4420
+ AND attempt >= 1 AND started_at IS NOT NULL)
4421
+ )
4422
+ )
4423
+ ),
4424
+ CHECK (status != 'succeeded' OR error IS NULL),
4425
+ -- A run is superseded instead of materialized, so it carries no failure, and only replacement
4426
+ -- runs can be: telemetry appends readings rather than replace a source.
4427
+ CHECK (status != 'superseded' OR (error IS NULL AND materialization_protocol = 'replacement')),
4428
+ CHECK ((projection_kind = 'telemetry') = (materialization_protocol = 'telemetry')),
4429
+ CHECK (
4430
+ (projection_kind = 'link' AND object_type_id IS NULL
4431
+ AND source_object_type_id IS NOT NULL AND target_object_type_id IS NOT NULL)
4432
+ OR (projection_kind IN ('object', 'telemetry') AND object_type_id IS NOT NULL
4433
+ AND source_object_type_id IS NULL AND target_object_type_id IS NULL)
4434
+ ),
4435
+ CHECK (
4436
+ (materialization_protocol = 'replacement' AND fixed_batch_size IS NULL
4437
+ AND next_batch_ordinal IS NULL AND next_row_offset IS NULL AND input_exhausted IS NULL)
4438
+ OR (materialization_protocol = 'telemetry' AND fixed_batch_size IS NOT NULL
4439
+ AND next_batch_ordinal IS NOT NULL AND next_row_offset IS NOT NULL
4440
+ AND input_exhausted IS NOT NULL)
4441
+ ),
4442
+ CHECK (
4443
+ (missing_target_object_type_id IS NULL AND missing_target_object_id IS NULL
4444
+ AND missing_target_batch_ordinal IS NULL AND missing_target_first_seen_at IS NULL)
4445
+ OR (projection_kind = 'telemetry' AND missing_target_object_type_id = object_type_id
4446
+ AND missing_target_object_id IS NOT NULL
4447
+ AND missing_target_batch_ordinal = next_batch_ordinal
4448
+ AND missing_target_first_seen_at IS NOT NULL)
4449
+ ),
4450
+ CHECK (projection_kind != 'telemetry' OR status != 'succeeded' OR input_exhausted = 1)
4451
+ );
4452
+
4453
+ INSERT INTO projection_runs_v2 (
4454
+ project_id, id, execution_id, projection_id, projection_kind, materialization_protocol,
4455
+ dataset_id, dataset_version_id, dataset_version_created_at, ontology_revision,
4456
+ projection_revision, ownership_hash, object_type_id, source_object_type_id,
4457
+ target_object_type_id, status, queued_at, started_at, finished_at, attempt, execution_token,
4458
+ fixed_batch_size, next_batch_ordinal, next_row_offset, input_exhausted,
4459
+ missing_target_object_type_id, missing_target_object_id, missing_target_batch_ordinal,
4460
+ missing_target_first_seen_at, source_rows_read, source_rows_skipped, source_changes_read, error
4461
+ )
4462
+ SELECT
4463
+ project_id, id, execution_id, projection_id, projection_kind, materialization_protocol,
4464
+ dataset_id, dataset_version_id, dataset_version_created_at, ontology_revision,
4465
+ projection_revision, ownership_hash, object_type_id, source_object_type_id,
4466
+ target_object_type_id, status, queued_at, started_at, finished_at, attempt, execution_token,
4467
+ fixed_batch_size, next_batch_ordinal, next_row_offset, input_exhausted,
4468
+ missing_target_object_type_id, missing_target_object_id, missing_target_batch_ordinal,
4469
+ missing_target_first_seen_at, source_rows_read, source_rows_skipped, source_changes_read, error
4470
+ FROM projection_runs;
4471
+
4472
+ DROP TABLE projection_runs;
4473
+ ALTER TABLE projection_runs_v2 RENAME TO projection_runs;
4474
+
4475
+ CREATE INDEX idx_projection_runs_project_started
4476
+ ON projection_runs(project_id, COALESCE(started_at, queued_at) DESC);
4477
+ CREATE INDEX idx_projection_runs_project_projection_started
4478
+ ON projection_runs(project_id, projection_id, COALESCE(started_at, queued_at) DESC);
4479
+ CREATE INDEX idx_projection_runs_project_dataset_started
4480
+ ON projection_runs(project_id, dataset_id, COALESCE(started_at, queued_at) DESC);
4481
+ CREATE INDEX idx_projection_runs_project_version_started
4482
+ ON projection_runs(project_id, dataset_version_id, COALESCE(started_at, queued_at) DESC);
4483
+ CREATE INDEX idx_projection_runs_project_status_started
4484
+ ON projection_runs(project_id, status, COALESCE(started_at, queued_at) DESC);
4485
+ CREATE INDEX idx_projection_runs_project_object_type_started
4486
+ ON projection_runs(project_id, object_type_id, COALESCE(started_at, queued_at) DESC);
4487
+ `;
4488
+
4489
+ // src/migrations/052-compact-source-storage.sql
4490
+ var _052_compact_source_storage_default = `-- Source versions get a compact surrogate key; roots and rows stop repeating the text triple.
4491
+ -- SQLite cannot add a key column to a table, so the three source tables are rebuilt. Index
4492
+ -- names are schema-wide: the old ones are dropped first to free them.
4493
+ DROP INDEX idx_ontology_sources_active;
4494
+ DROP INDEX idx_ontology_sources_run_candidate;
4495
+ DROP INDEX idx_ontology_sources_cleanup;
4496
+ DROP INDEX idx_ontology_sources_run;
4497
+ DROP INDEX idx_ontology_source_roots_active;
4498
+ DROP INDEX idx_ontology_source_roots_lookup;
4499
+ DROP INDEX idx_ontology_source_roots_cleanup;
4500
+ DROP INDEX idx_ontology_source_rows_root;
4501
+ DROP INDEX idx_ontology_source_rows_staging_ordinal;
4502
+ DROP INDEX idx_ontology_source_rows_entity_sort;
4503
+ DROP INDEX idx_ontology_source_rows_object;
4504
+ DROP INDEX idx_ontology_source_rows_link_source;
4505
+ DROP INDEX idx_ontology_source_rows_link_target;
4506
+ ALTER TABLE ontology_source_rows RENAME TO legacy_ontology_source_rows;
4507
+ ALTER TABLE ontology_source_roots RENAME TO legacy_ontology_source_roots;
4508
+ ALTER TABLE ontology_sources RENAME TO legacy_ontology_sources;
4509
+
4510
+ CREATE TABLE ontology_sources (
4511
+ version_id INTEGER PRIMARY KEY,
4512
+ project_id TEXT NOT NULL,
4513
+ source_id TEXT NOT NULL,
4514
+ materialization_id TEXT NOT NULL,
4515
+ projection_run_id TEXT NOT NULL,
4516
+ projection_kind TEXT NOT NULL CHECK (projection_kind IN ('object', 'link')),
4517
+ protocol TEXT NOT NULL CHECK (protocol = 'replacement'),
4518
+ status TEXT NOT NULL CHECK (
4519
+ status IN ('staging', 'ready', 'active', 'superseded', 'abandoned')
4520
+ ),
4521
+ execution_token TEXT,
4522
+ dataset_id TEXT NOT NULL,
4523
+ dataset_version_id TEXT NOT NULL,
4524
+ dataset_version_created_at TEXT NOT NULL,
4525
+ projection_revision TEXT NOT NULL,
4526
+ ownership_hash TEXT NOT NULL,
4527
+ ontology_revision TEXT NOT NULL,
4528
+ root_count INTEGER CHECK (root_count IS NULL OR root_count >= 0),
4529
+ assertion_count INTEGER CHECK (assertion_count IS NULL OR assertion_count >= 0),
4530
+ created_at TEXT NOT NULL,
4531
+ ready_at TEXT,
4532
+ activated_at TEXT,
4533
+ terminal_at TEXT,
4534
+ last_commit_id TEXT,
4535
+ updated_at TEXT NOT NULL,
4536
+ base_materialization_id TEXT,
4537
+ base_commit_id TEXT,
4538
+ UNIQUE (project_id, source_id, materialization_id),
4539
+ CHECK ((base_materialization_id IS NULL) = (base_commit_id IS NULL)),
4540
+ CHECK ((root_count IS NULL) = (assertion_count IS NULL)),
4541
+ CHECK ((root_count IS NULL) = (ready_at IS NULL)),
4542
+ CHECK ((status IN ('staging', 'ready')) = (execution_token IS NOT NULL)),
4543
+ CHECK (
4544
+ (status = 'staging' AND ready_at IS NULL)
4545
+ OR (status IN ('ready', 'active', 'superseded') AND ready_at IS NOT NULL)
4546
+ OR status = 'abandoned'
4547
+ ),
4548
+ CHECK ((status IN ('active', 'superseded')) = (activated_at IS NOT NULL)),
4549
+ CHECK ((status IN ('active', 'superseded')) = (last_commit_id IS NOT NULL)),
4550
+ CHECK ((status IN ('superseded', 'abandoned')) = (terminal_at IS NOT NULL)),
4551
+ CHECK (ready_at IS NULL OR created_at <= ready_at),
4552
+ CHECK (activated_at IS NULL OR (ready_at IS NOT NULL AND ready_at <= activated_at)),
4553
+ CHECK (terminal_at IS NULL OR created_at <= terminal_at),
4554
+ CHECK (terminal_at IS NULL OR ready_at IS NULL OR ready_at <= terminal_at),
4555
+ CHECK (terminal_at IS NULL OR activated_at IS NULL OR activated_at <= terminal_at),
4556
+ CHECK (created_at <= updated_at)
4557
+ );
4558
+ INSERT INTO ontology_sources (
4559
+ project_id, source_id, materialization_id, projection_run_id, projection_kind, protocol,
4560
+ status, execution_token, dataset_id, dataset_version_id, dataset_version_created_at,
4561
+ projection_revision, ownership_hash, ontology_revision, root_count, assertion_count,
4562
+ created_at, ready_at, activated_at, terminal_at, last_commit_id, updated_at,
4563
+ base_materialization_id, base_commit_id
4564
+ )
4565
+ SELECT project_id, source_id, materialization_id, projection_run_id, projection_kind, protocol,
4566
+ status, execution_token, dataset_id, dataset_version_id, dataset_version_created_at,
4567
+ projection_revision, ownership_hash, ontology_revision, root_count, assertion_count,
4568
+ created_at, ready_at, activated_at, terminal_at, last_commit_id, updated_at,
4569
+ base_materialization_id, base_commit_id
4570
+ FROM legacy_ontology_sources
4571
+ ORDER BY rowid;
4572
+ CREATE UNIQUE INDEX idx_ontology_sources_active
4573
+ ON ontology_sources(project_id, source_id)
4574
+ WHERE status = 'active';
4575
+ CREATE UNIQUE INDEX idx_ontology_sources_run_candidate
4576
+ ON ontology_sources(project_id, projection_run_id)
4577
+ WHERE status IN ('staging', 'ready');
4578
+ CREATE INDEX idx_ontology_sources_cleanup
4579
+ ON ontology_sources(project_id, status, terminal_at, source_id, materialization_id);
4580
+ CREATE INDEX idx_ontology_sources_run
4581
+ ON ontology_sources(project_id, projection_run_id);
4582
+
4583
+ -- A root is live while its version is published (active or superseded), it is not retired and
4584
+ -- it is not a deletion. Activation therefore writes only the roots it replaces, never its own.
4585
+ -- \`root_key\` is the canonical key; SQLite's default BINARY collation orders it by its UTF-8 bytes.
4586
+ CREATE TABLE ontology_source_roots (
4587
+ id INTEGER PRIMARY KEY,
4588
+ version_id INTEGER NOT NULL REFERENCES ontology_sources (version_id) ON DELETE RESTRICT,
4589
+ project_id TEXT NOT NULL,
4590
+ root_key TEXT NOT NULL,
4591
+ staging_ordinal INTEGER NOT NULL CHECK (staging_ordinal >= 0),
4592
+ deleted INTEGER NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
4593
+ retired_at TEXT,
4594
+ CONSTRAINT ontology_source_roots_key UNIQUE (version_id, root_key),
4595
+ CONSTRAINT ontology_source_roots_ordinal UNIQUE (version_id, staging_ordinal)
4596
+ );
4597
+ CREATE INDEX idx_ontology_source_roots_live
4598
+ ON ontology_source_roots (project_id, root_key) WHERE retired_at IS NULL AND deleted = 0;
4599
+ CREATE INDEX idx_ontology_source_roots_retired
4600
+ ON ontology_source_roots (project_id, retired_at) WHERE retired_at IS NOT NULL;
4601
+
4602
+ -- A row's identity lives in its typed columns; \`payload\` is the assertion without kind and ref.
4603
+ CREATE TABLE ontology_source_rows (
4604
+ root_id INTEGER NOT NULL REFERENCES ontology_source_roots (id) ON DELETE RESTRICT,
4605
+ entity_kind TEXT NOT NULL CHECK (entity_kind IN ('object', 'link')),
4606
+ object_type_id TEXT,
4607
+ primary_id TEXT,
4608
+ source_type_id TEXT,
4609
+ source_primary_id TEXT,
4610
+ link_id TEXT,
4611
+ target_type_id TEXT,
4612
+ target_primary_id TEXT,
4613
+ payload TEXT CHECK (json_valid(payload)),
4614
+ CHECK (
4615
+ (
4616
+ entity_kind = 'object'
4617
+ AND object_type_id IS NOT NULL AND primary_id IS NOT NULL
4618
+ AND source_type_id IS NULL AND source_primary_id IS NULL AND link_id IS NULL
4619
+ AND target_type_id IS NULL AND target_primary_id IS NULL
4620
+ )
4621
+ OR
4622
+ (
4623
+ entity_kind = 'link'
4624
+ AND object_type_id IS NULL AND primary_id IS NULL
4625
+ AND source_type_id IS NOT NULL AND source_primary_id IS NOT NULL AND link_id IS NOT NULL
4626
+ AND target_type_id IS NOT NULL AND target_primary_id IS NOT NULL
4627
+ )
4628
+ )
4629
+ );
4630
+ CREATE INDEX idx_ontology_source_rows_root ON ontology_source_rows (root_id);
4631
+ CREATE INDEX idx_ontology_source_rows_link_source
4632
+ ON ontology_source_rows (source_type_id, source_primary_id, link_id)
4633
+ WHERE entity_kind = 'link';
4634
+ CREATE INDEX idx_ontology_source_rows_link_target
4635
+ ON ontology_source_rows (target_type_id, target_primary_id)
4636
+ WHERE entity_kind = 'link';
4637
+
4638
+ -- A published root that was not active is retired, so liveness stays exactly what it was.
4639
+ INSERT INTO ontology_source_roots (
4640
+ version_id, project_id, root_key, staging_ordinal, deleted, retired_at
4641
+ )
4642
+ SELECT versions.version_id, roots.project_id, roots.root_key, roots.staging_ordinal,
4643
+ roots.deleted,
4644
+ CASE
4645
+ WHEN roots.active = 1 THEN NULL
4646
+ WHEN roots.retired_at IS NOT NULL THEN roots.retired_at
4647
+ WHEN versions.status IN ('active', 'superseded') THEN versions.updated_at
4648
+ END
4649
+ FROM legacy_ontology_source_roots AS roots
4650
+ JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id)
4651
+ ORDER BY versions.version_id, roots.staging_ordinal;
4652
+
4653
+ INSERT INTO ontology_source_rows (
4654
+ root_id, entity_kind, object_type_id, primary_id,
4655
+ source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
4656
+ )
4657
+ SELECT roots.id, rows.entity_kind, rows.object_type_id, rows.primary_id,
4658
+ rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
4659
+ rows.target_primary_id, NULLIF(json_remove(rows.assertion, '$.kind', '$.ref'), '{}')
4660
+ FROM legacy_ontology_source_rows AS rows
4661
+ JOIN ontology_sources AS versions USING (project_id, source_id, materialization_id)
4662
+ JOIN ontology_source_roots AS roots
4663
+ ON roots.version_id = versions.version_id AND roots.root_key = rows.root_key
4664
+ ORDER BY roots.id;
4665
+
4666
+ DROP TABLE legacy_ontology_source_rows;
4667
+ DROP TABLE legacy_ontology_source_roots;
4668
+ DROP TABLE legacy_ontology_sources;
4669
+ -- No ANALYZE: nothing refreshes SQLite statistics afterwards, and on a fresh install they would
4670
+ -- describe empty tables forever and steer the planner to partial indexes over primary keys.
4671
+ `;
4672
+
4360
4673
  // src/migrations/037-execution-requester-groups.sql
4361
4674
  var _037_execution_requester_groups_default = `ALTER TABLE executions ADD COLUMN requester_group_ids TEXT NOT NULL DEFAULT '[]';
4362
4675
 
@@ -4561,7 +4874,16 @@ var sqliteStorageMigrations = defineMigrations({
4561
4874
  sqliteSql("047-ontology-commit-attribution", _047_ontology_commit_attribution_default),
4562
4875
  sqliteSql("048-file-upload-sessions", _048_file_upload_sessions_default),
4563
4876
  sqliteSql("049-native-sessions", _049_native_sessions_default),
4564
- sqliteSql("050-directory-group-memberships", _050_directory_group_memberships_default)
4877
+ sqliteSql("050-directory-group-memberships", _050_directory_group_memberships_default),
4878
+ sqliteSql("051-projection-run-supersession", _051_projection_run_supersession_default),
4879
+ sqliteStep("052-compact-source-storage", (db) => {
4880
+ const before = sourceStorageCounts(db);
4881
+ db.run(_052_compact_source_storage_default);
4882
+ const after = sourceStorageCounts(db);
4883
+ if (after.versions !== before.versions || after.roots !== before.roots || after.rows !== before.rows) {
4884
+ throw new Error("[SixbSqliteStorage] Source versions, roots or rows were lost while compacting source storage.");
4885
+ }
4886
+ }, { checksum: checksum(_052_compact_source_storage_default) })
4565
4887
  ]
4566
4888
  });
4567
4889
  function sqliteStoragePath(basePath) {
@@ -4676,6 +4998,11 @@ function rowToMigrationRecord(row) {
4676
4998
  finishedAt: migration.finished_at ?? undefined
4677
4999
  };
4678
5000
  }
5001
+ function sourceStorageCounts(db) {
5002
+ return db.query(`SELECT (SELECT count(*) FROM ontology_sources) AS versions,
5003
+ (SELECT count(*) FROM ontology_source_roots) AS roots,
5004
+ (SELECT count(*) FROM ontology_source_rows) AS rows`).get();
5005
+ }
4679
5006
  function checksum(value) {
4680
5007
  return createHash("sha256").update(value).digest("hex");
4681
5008
  }
@@ -10240,7 +10567,7 @@ var PAGE_TOKEN_PREFIX = "keyset:";
10240
10567
  var DEFAULT_EXPANSION_FANOUT = 1000;
10241
10568
  function compileObjectQuery(projectId, query, options = {}) {
10242
10569
  if (hasVectorProfile(query) && !isVectorProfileQuery(query)) {
10243
- throw new ObjectQueryExecutionError("unsupported_vector_composition", "Vector search supports one profile with filters before ranking and limit/project after.");
10570
+ throw new ObjectQueryExecutionError("unsupported_vector_composition", "Vector search supports one profile with filters, traversals or sets before ranking and limit/project after.");
10244
10571
  }
10245
10572
  const source = options.source ?? DEFAULT_OBJECT_QUERY_SOURCE;
10246
10573
  const ctx = { probeLimit: options.includeTotal === false, source };
@@ -10313,9 +10640,29 @@ function compileVector(projectId, query, ctx) {
10313
10640
  { kind: "column", column: "_vector_score", direction: "desc" },
10314
10641
  ...identityOrderFields().map((field) => ({ ...field, binary: true }))
10315
10642
  ]);
10316
- const sql = `SELECT * FROM (SELECT input.*, max(-1, min(1, 1 - vec_distance_cosine(vectors.embedding, ?))) AS _vector_score
10317
- ${candidates}) AS ranked ORDER BY ${order.sql} LIMIT ?`;
10318
- const args = [encodeSqliteVector(values), ...candidateArgs, query.k];
10643
+ const rankedOrder = compileOrder(order.fields, "ranked");
10644
+ const sql = `SELECT stored.project_id, stored.object_type_id, stored.primary_id,
10645
+ stored.properties, stored.properties AS _cursor_properties,
10646
+ stored.created_at, stored.updated_at, stored.version, stored.last_commit_id,
10647
+ ranked._vector_candidate_count, ranked._vector_score
10648
+ FROM (
10649
+ WITH _sixb_vector_candidates AS MATERIALIZED (
10650
+ SELECT input.project_id, input.object_type_id, input.primary_id,
10651
+ vectors.embedding AS _vector_embedding ${candidates} LIMIT ?
10652
+ ), _sixb_vector_budget AS (
10653
+ SELECT COUNT(*) AS _vector_candidate_count FROM _sixb_vector_candidates
10654
+ )
10655
+ SELECT input.project_id, input.object_type_id, input.primary_id, budget._vector_candidate_count,
10656
+ CASE WHEN budget._vector_candidate_count <= ?
10657
+ THEN max(-1, min(1, 1 - vec_distance_cosine(input._vector_embedding, ?)))
10658
+ END AS _vector_score
10659
+ FROM _sixb_vector_candidates AS input CROSS JOIN _sixb_vector_budget AS budget
10660
+ ORDER BY ${order.sql} LIMIT ?
10661
+ ) AS ranked CROSS JOIN ${ctx.source.objectsTable} AS stored
10662
+ ON stored.project_id = ranked.project_id AND stored.object_type_id = ranked.object_type_id
10663
+ AND stored.primary_id = ranked.primary_id
10664
+ ORDER BY ${rankedOrder.sql}`;
10665
+ const args = [...candidateArgs, limit + 1, limit, encodeSqliteVector(values), query.k];
10319
10666
  return {
10320
10667
  sql,
10321
10668
  args,
@@ -10325,7 +10672,9 @@ function compileVector(projectId, query, ctx) {
10325
10672
  vectorProbe: {
10326
10673
  sql: `SELECT COUNT(*) AS total FROM (SELECT 1 ${candidates} LIMIT ?) AS candidates`,
10327
10674
  args: [...candidateArgs, limit + 1],
10328
- limit
10675
+ limit,
10676
+ rowLimit: query.k,
10677
+ totalLimit: query.k
10329
10678
  },
10330
10679
  hasMore: () => false,
10331
10680
  trimRows: identityRows,
@@ -10484,7 +10833,11 @@ function compileLimit(projectId, inputQuery, rawLimit, ctx) {
10484
10833
  FROM (${input.sql}) AS input
10485
10834
  `,
10486
10835
  totalArgs: input.args,
10487
- vectorProbe: input.vectorProbe,
10836
+ vectorProbe: input.vectorProbe ? {
10837
+ ...input.vectorProbe,
10838
+ rowLimit: Math.min(input.vectorProbe.rowLimit, rowLimit),
10839
+ totalLimit: input.vectorProbe.rowLimit
10840
+ } : undefined,
10488
10841
  order: input.order,
10489
10842
  hasMore: (rowCount, total) => total === undefined ? ctx.probeLimit && rowCount > limit : limit < total,
10490
10843
  trimRows: ctx.probeLimit ? (rows) => rows.slice(0, limit) : identityRows,
@@ -10761,7 +11114,7 @@ function compileProject(projectId, inputQuery, properties, ctx) {
10761
11114
  input.created_at,
10762
11115
  input.updated_at,
10763
11116
  input.version,
10764
- input.last_commit_id${input.vectorProbe ? ", input._vector_score" : ""}
11117
+ input.last_commit_id${input.vectorProbe ? ", input._vector_score, input._vector_candidate_count" : ""}
10765
11118
  FROM (${input.sql}) AS input
10766
11119
  ORDER BY ${inputOrder.sql}
10767
11120
  `,
@@ -11305,19 +11658,20 @@ class SqliteObjectReader {
11305
11658
  ensureSqliteVectorSearch(this.db);
11306
11659
  const probe = compiled.vectorProbe;
11307
11660
  const row = this.db.query(probe.sql).get(...probe.args);
11308
- if (Number(row?.total ?? 0) > probe.limit) {
11309
- throw new ObjectQueryExecutionError3("vector_work_limit_exceeded", `Exact vector search supports at most ${probe.limit} eligible vectors at this dimension. Narrow the query filters.`);
11310
- }
11661
+ assertVectorCandidateCount(row?.total, probe.limit);
11311
11662
  }
11312
11663
  queryObjects(params) {
11313
- this.assertVectorSearch(params);
11314
11664
  const source = this.source;
11315
11665
  const compiled = compileObjectQuery(params.projectId, params.query, {
11316
11666
  includeTotal: params.includeTotal,
11317
11667
  source
11318
11668
  });
11319
- const total = params.includeTotal === false ? undefined : readTotal(this.db, compiled);
11669
+ if (compiled.vectorProbe)
11670
+ ensureSqliteVectorSearch(this.db);
11671
+ const ordinaryTotal = params.includeTotal === false || compiled.vectorProbe ? undefined : readTotal(this.db, compiled);
11320
11672
  const rawRows = this.db.query(compiled.sql).all(...compiled.args);
11673
+ const vectorTotal = this.vectorResultTotal(compiled, rawRows);
11674
+ const total = params.includeTotal === false ? undefined : vectorTotal ?? ordinaryTotal;
11321
11675
  const rows = compiled.trimRows(rawRows);
11322
11676
  const hasMore = total === undefined && compiled.hasMoreProbe ? compiled.hasMoreProbe.hasMore(this.db.query(compiled.hasMoreProbe.sql).all(...compiled.hasMoreProbe.args).length) : compiled.hasMore(rawRows.length, total);
11323
11677
  return {
@@ -11327,6 +11681,17 @@ class SqliteObjectReader {
11327
11681
  ...total === undefined ? {} : { total }
11328
11682
  };
11329
11683
  }
11684
+ vectorResultTotal(compiled, rows) {
11685
+ const probe = compiled.vectorProbe;
11686
+ if (!probe)
11687
+ return;
11688
+ let count = rows[0]?._vector_candidate_count;
11689
+ if (rows.length === 0) {
11690
+ count = probe.rowLimit === 0 ? this.db.query(probe.sql).get(...probe.args)?.total : 0;
11691
+ }
11692
+ assertVectorCandidateCount(count, probe.limit);
11693
+ return Math.min(count, probe.totalLimit);
11694
+ }
11330
11695
  countObjects(params) {
11331
11696
  this.assertVectorSearch(params);
11332
11697
  const source = this.source;
@@ -11640,6 +12005,14 @@ function sqliteFacetValue(valueType, value) {
11640
12005
  }
11641
12006
  return value;
11642
12007
  }
12008
+ function assertVectorCandidateCount(count, limit) {
12009
+ if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
12010
+ throw new ObjectQueryExecutionError3("invalid_vector_candidate_count", "Storage did not return a valid vector candidate count.");
12011
+ }
12012
+ if (count > limit) {
12013
+ throw new ObjectQueryExecutionError3("vector_work_limit_exceeded", `Exact vector search supports at most ${limit} eligible vectors at this dimension. Narrow the query filters.`);
12014
+ }
12015
+ }
11643
12016
  function existsProbeQuery(query) {
11644
12017
  return { kind: "limit", limit: 1, input: stripOuterRowShape(query) };
11645
12018
  }
@@ -11731,7 +12104,7 @@ function compileSelectedReadScopeCte(scopeJson, projectId) {
11731
12104
  _sixb_scope_root_facts(project_id, root_id, node_id, object_type_id, primary_id) AS (
11732
12105
  SELECT anchor.project_id, root.root_id, root.node_id, root.object_type_id, root.primary_id
11733
12106
  FROM _sixb_scope_roots AS root
11734
- JOIN objects AS anchor
12107
+ CROSS JOIN objects AS anchor
11735
12108
  ON anchor.project_id = ?
11736
12109
  AND anchor.object_type_id = root.object_type_id
11737
12110
  AND anchor.primary_id = root.primary_id
@@ -11813,7 +12186,7 @@ function compileSelectedReadScopeCte(scopeJson, projectId) {
11813
12186
  GROUP BY reachable.project_id, reachable.object_type_id, reachable.primary_id
11814
12187
  ),
11815
12188
  _sixb_scope_objects AS (
11816
- SELECT stored.project_id, stored.object_type_id, stored.primary_id,
12189
+ SELECT grants.project_id, grants.object_type_id, grants.primary_id,
11817
12190
  COALESCE((
11818
12191
  SELECT json_group_object(property.key, ${sqliteJsonEachValue("property")})
11819
12192
  FROM json_each(stored.properties) AS property
@@ -11903,12 +12276,12 @@ function compileSelectedReadScopeCte(scopeJson, projectId) {
11903
12276
  ),
11904
12277
  _sixb_scope_links AS (
11905
12278
  SELECT
11906
- stored.project_id,
11907
- stored.source_type_id,
11908
- stored.source_id,
11909
- stored.link_id,
11910
- stored.target_type_id,
11911
- stored.target_id,
12279
+ visible.project_id,
12280
+ visible.source_type_id,
12281
+ visible.source_id,
12282
+ visible.link_id,
12283
+ visible.target_type_id,
12284
+ visible.target_id,
11912
12285
  CASE
11913
12286
  WHEN stored.properties IS NULL THEN NULL
11914
12287
  ELSE NULLIF(
@@ -12204,6 +12577,10 @@ function assertReconciliationPageLimit(limit) {
12204
12577
  // src/ontology-storage/shared.ts
12205
12578
  import { parseSixbFailure as parseSixbFailure2 } from "@sixb/core/internal/errors";
12206
12579
  import { MaterializationConflictError } from "@sixb/core/internal/materialization";
12580
+ import {
12581
+ sourceAssertionFromColumns,
12582
+ sourceEntityFromKey
12583
+ } from "@sixb/core/internal/ontology-storage-provider";
12207
12584
  import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage";
12208
12585
  import {
12209
12586
  assertNonblank,
@@ -12256,8 +12633,8 @@ function sourceRecord(row) {
12256
12633
  }
12257
12634
  function sourceAssertion(row) {
12258
12635
  return {
12259
- root: parseJson2(row.root),
12260
- assertion: parseJson2(row.assertion),
12636
+ root: sourceEntityFromKey(row.root_key),
12637
+ assertion: sourceAssertionFromColumns(row, row.payload === null ? null : parseJson2(row.payload)),
12261
12638
  stagingOrdinal: row.staging_ordinal
12262
12639
  };
12263
12640
  }
@@ -12475,29 +12852,68 @@ import {
12475
12852
  MaterializationValidationError,
12476
12853
  projectionEntityKey
12477
12854
  } from "@sixb/core/internal/materialization";
12478
- import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider";
12479
- function replacementSourceRows(input) {
12480
- const roots = input.incremental ? `ontology_source_roots AS changed
12481
- CROSS JOIN ontology_source_roots AS roots ON roots.project_id = changed.project_id
12482
- AND roots.source_id = changed.source_id AND roots.root_sort_key = changed.root_sort_key AND roots.active = 1` : "ontology_source_roots AS roots";
12483
- const selected = input.incremental ? "changed" : "roots";
12855
+ import {
12856
+ sourceEntityColumns,
12857
+ sourceStageRoots
12858
+ } from "@sixb/core/internal/ontology-storage-provider";
12859
+ var PUBLISHED = "('active', 'superseded')";
12860
+ var SOURCE_ASSERTION_COLUMNS = `versions.source_id, versions.materialization_id,
12861
+ roots.root_key, roots.staging_ordinal, rows.entity_kind, rows.object_type_id, rows.primary_id,
12862
+ rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
12863
+ rows.target_primary_id, rows.payload`;
12864
+ var LIVE_SOURCE_ROWS_JOIN = `
12865
+ CROSS JOIN ontology_source_roots AS roots ON roots.id = rows.root_id
12866
+ AND roots.project_id = ? AND roots.retired_at IS NULL AND roots.deleted = 0
12867
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12868
+ AND versions.status IN ${PUBLISHED}`;
12869
+ var ENTITY_MATCH = `rows.entity_kind = requested.entity_kind
12870
+ AND rows.object_type_id IS requested.object_type_id
12871
+ AND rows.primary_id IS requested.primary_id
12872
+ AND rows.source_type_id IS requested.source_type_id
12873
+ AND rows.source_primary_id IS requested.source_primary_id
12874
+ AND rows.link_id IS requested.link_id
12875
+ AND rows.target_type_id IS requested.target_type_id
12876
+ AND rows.target_primary_id IS requested.target_primary_id`;
12877
+ var REQUESTED_ENTITY = `json_extract(value, '$.entityKind') AS entity_kind,
12878
+ json_extract(value, '$.objectTypeId') AS object_type_id,
12879
+ json_extract(value, '$.primaryId') AS primary_id,
12880
+ json_extract(value, '$.sourceTypeId') AS source_type_id,
12881
+ json_extract(value, '$.sourcePrimaryId') AS source_primary_id,
12882
+ json_extract(value, '$.linkId') AS link_id,
12883
+ json_extract(value, '$.targetTypeId') AS target_type_id,
12884
+ json_extract(value, '$.targetPrimaryId') AS target_primary_id`;
12885
+ function requestedEntity(ref) {
12886
+ const key = projectionEntityKey(ref);
12484
12887
  return {
12485
- sql: `SELECT * FROM ontology_source_rows WHERE project_id = ? AND source_id = ? AND materialization_id = ?
12888
+ entityKind: ref.kind,
12889
+ ...sourceEntityColumns(ref),
12890
+ rootKeys: ref.kind === "object" ? [key] : [key, projectionEntityKey({ kind: "object", ref: ref.ref.source })]
12891
+ };
12892
+ }
12893
+ function sourceVersionId(db, input) {
12894
+ const version = db.query(`SELECT version_id FROM ontology_sources
12895
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?`).get(input.projectId, input.sourceId, input.materializationId);
12896
+ return version?.version_id ?? null;
12897
+ }
12898
+ function replacementSourceRows(db, input) {
12899
+ const version = sourceVersionId(db, input);
12900
+ const live = `live.retired_at IS NULL AND live.deleted = 0
12901
+ AND versions.source_id = ? AND versions.status IN ${PUBLISHED}`;
12902
+ const replaced = input.incremental ? `SELECT live.id FROM ontology_source_roots AS changed
12903
+ CROSS JOIN ontology_source_roots AS live
12904
+ ON live.project_id = ? AND live.root_key = changed.root_key
12905
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = live.version_id
12906
+ WHERE changed.version_id = ? AND ${live}` : `SELECT live.id FROM ontology_sources AS versions
12907
+ CROSS JOIN ontology_source_roots AS live ON live.version_id = versions.version_id
12908
+ WHERE versions.project_id = ? AND ${live}`;
12909
+ return {
12910
+ sql: `SELECT rows.* FROM ontology_source_roots AS roots
12911
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
12912
+ WHERE roots.version_id = ?
12486
12913
  UNION ALL
12487
- SELECT rows.* FROM ${roots}
12488
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id
12489
- AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
12490
- AND rows.root_sort_key = roots.root_sort_key
12491
- WHERE ${selected}.project_id = ? AND ${selected}.source_id = ?
12492
- AND ${input.incremental ? "changed.materialization_id = ?" : "roots.active = 1"}`,
12493
- values: [
12494
- input.projectId,
12495
- input.sourceId,
12496
- input.materializationId,
12497
- input.projectId,
12498
- input.sourceId,
12499
- ...input.incremental ? [input.materializationId] : []
12500
- ]
12914
+ SELECT rows.* FROM (${replaced}) AS roots
12915
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id`,
12916
+ values: [version, input.projectId, ...input.incremental ? [version] : [], input.sourceId]
12501
12917
  };
12502
12918
  }
12503
12919
  function stageSourceRoots(db, manifest, input) {
@@ -12506,15 +12922,15 @@ function stageSourceRoots(db, manifest, input) {
12506
12922
  throw new MaterializationValidationError("Root deletions require a source delta base.");
12507
12923
  }
12508
12924
  const existing = db.query(`SELECT staging_ordinal, deleted FROM ontology_source_roots
12509
- WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND root_sort_key = ?`);
12925
+ WHERE version_id = ? AND root_key = ?`);
12510
12926
  const insert = db.query(`INSERT INTO ontology_source_roots (
12511
- project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
12512
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING root_sort_key`);
12927
+ version_id, project_id, root_key, staging_ordinal, deleted
12928
+ ) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING id`);
12513
12929
  for (const root of roots) {
12514
- const inserted = insert.get(input.projectId, input.source.projectionId, input.materializationId, root.sortKey, root.root.kind, root.rootKey, canonicalJson(root.root), root.stagingOrdinal, Number(root.deleted));
12930
+ const inserted = insert.get(manifest.version_id, input.projectId, root.rootKey, root.stagingOrdinal, Number(root.deleted));
12515
12931
  if (inserted)
12516
12932
  continue;
12517
- const previous = existing.get(input.projectId, input.source.projectionId, input.materializationId, root.sortKey);
12933
+ const previous = existing.get(manifest.version_id, root.rootKey);
12518
12934
  if (!previous || previous.staging_ordinal !== root.stagingOrdinal || Boolean(previous.deleted) !== root.deleted) {
12519
12935
  throw new MaterializationValidationError("Source materialization repeats root or stream ordinal with different content.");
12520
12936
  }
@@ -12525,72 +12941,61 @@ function activateSourceRoots(db, projectId, candidate, activation) {
12525
12941
  throw new MaterializationConflictError2("projection-fence", "Source delta base changed before activation.");
12526
12942
  if (candidate.base_materialization_id !== null && candidate.root_count === 0)
12527
12943
  return;
12528
- const scope = [projectId, activation.source.projectionId, activation.materializationId];
12529
- db.query(`UPDATE ontology_source_roots AS roots SET active = 0, retired_at = ?
12530
- WHERE project_id = ? AND source_id = ? AND active = 1
12531
- ${candidate.base_materialization_id === null ? "" : `AND root_sort_key IN (
12532
- SELECT root_sort_key FROM ontology_source_roots
12533
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?)`}`).run(activation.updatedAt, projectId, activation.source.projectionId, ...candidate.base_materialization_id === null ? [] : [...scope]);
12534
- db.query(`UPDATE ontology_source_roots SET active = CASE WHEN deleted = 1 THEN 0 ELSE 1 END,
12535
- retired_at = CASE WHEN deleted = 1 THEN ? ELSE NULL END
12536
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?`).run(activation.updatedAt, ...scope);
12944
+ const delta = candidate.base_materialization_id !== null;
12945
+ db.query(`UPDATE ontology_source_roots SET retired_at = ?
12946
+ WHERE retired_at IS NULL AND deleted = 0
12947
+ AND version_id IN (SELECT version_id FROM ontology_sources
12948
+ WHERE project_id = ? AND source_id = ? AND status IN ${PUBLISHED})
12949
+ ${delta ? `AND project_id = ? AND root_key IN (
12950
+ SELECT root_key FROM ontology_source_roots WHERE version_id = ?)` : ""}`).run(activation.updatedAt, projectId, activation.source.projectionId, ...delta ? [projectId, candidate.version_id] : []);
12951
+ db.query(`UPDATE ontology_source_roots SET retired_at = ?
12952
+ WHERE version_id = ? AND deleted = 1`).run(activation.updatedAt, candidate.version_id);
12537
12953
  }
12538
12954
  function assertSourceRootCoverage(db, manifest) {
12539
12955
  const invalid = db.query(`SELECT 1 FROM ontology_source_roots AS roots
12540
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
12956
+ WHERE roots.version_id = ?
12541
12957
  AND roots.deleted = EXISTS (
12542
- SELECT 1 FROM ontology_source_rows AS rows
12543
- WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12544
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
12545
- ) LIMIT 1`).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
12958
+ SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id
12959
+ ) LIMIT 1`).get(manifest.version_id);
12546
12960
  if (invalid)
12547
12961
  throw new MaterializationValidationError("Source roots must have complete assertions or an explicit deletion, never both.");
12548
12962
  }
12549
12963
  function activeSourceRows(db, projectId, refs) {
12550
- const keys = refs.flatMap((entity) => {
12551
- const entitySortKey = utf8SortKey(projectionEntityKey(entity));
12552
- const roots = entity.kind === "object" ? [entitySortKey] : [
12553
- entitySortKey,
12554
- utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source }))
12555
- ];
12556
- return roots.map((rootSortKey) => ({ rootSortKey, entitySortKey }));
12964
+ const requested = refs.flatMap((ref) => {
12965
+ const { rootKeys, ...entity } = requestedEntity(ref);
12966
+ return rootKeys.map((rootKey) => ({ ...entity, rootKey }));
12557
12967
  });
12558
12968
  return db.query(`WITH requested AS (
12559
- SELECT DISTINCT json_extract(value, '$.rootSortKey') AS root_sort_key,
12560
- json_extract(value, '$.entitySortKey') AS entity_sort_key FROM json_each(?)
12561
- ) SELECT rows.* FROM requested
12969
+ SELECT DISTINCT json_extract(value, '$.rootKey') AS root_key, ${REQUESTED_ENTITY}
12970
+ FROM json_each(?)
12971
+ ) SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
12562
12972
  CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
12563
- AND roots.active = 1 AND roots.root_sort_key = requested.root_sort_key
12564
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12565
- AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
12566
- AND rows.root_sort_key = roots.root_sort_key`).all(canonicalJson(keys), projectId);
12973
+ AND roots.root_key = requested.root_key AND roots.retired_at IS NULL AND roots.deleted = 0
12974
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12975
+ AND versions.status IN ${PUBLISHED}
12976
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}`).all(canonicalJson(requested), projectId);
12567
12977
  }
12568
12978
  function replacementAssertionRows(db, input) {
12569
- const keys = input.refs.map((ref) => ({
12570
- entityKind: ref.kind,
12571
- entityKey: projectionEntityKey(ref),
12572
- rootKeys: ref.kind === "object" ? [utf8SortKey(projectionEntityKey(ref))] : [
12573
- utf8SortKey(projectionEntityKey(ref)),
12574
- utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source }))
12575
- ]
12576
- }));
12979
+ const version = sourceVersionId(db, input);
12577
12980
  const previous = !input.includePrevious ? "" : `UNION ALL
12578
- SELECT rows.* FROM requested_entities AS requested
12579
- CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ? AND roots.source_id = ?
12580
- AND roots.active = 1 AND roots.root_sort_key IN (SELECT value FROM json_each(requested.root_keys))
12581
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12582
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
12583
- AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
12981
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested_entities AS requested
12982
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
12983
+ AND roots.root_key IN (SELECT value FROM json_each(requested.root_keys))
12984
+ AND roots.retired_at IS NULL AND roots.deleted = 0
12985
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12986
+ AND versions.source_id = ? AND versions.status IN ${PUBLISHED}
12987
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}
12584
12988
  ${!input.incremental ? "" : `WHERE EXISTS (SELECT 1 FROM ontology_source_roots AS changed
12585
- WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
12586
- AND changed.materialization_id = ? AND changed.root_sort_key = roots.root_sort_key)`}`;
12989
+ WHERE changed.version_id = ? AND changed.root_key = roots.root_key)`}`;
12587
12990
  return db.query(`WITH requested_entities AS (
12588
- SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
12589
- json_extract(value, '$.entityKey') AS entity_key, json_extract(value, '$.rootKeys') AS root_keys FROM json_each(?)
12590
- ) SELECT rows.* FROM requested_entities AS requested
12591
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = ? AND rows.source_id = ?
12592
- AND rows.materialization_id = ? AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
12593
- ${previous}`).all(canonicalJson(keys), input.projectId, input.sourceId, input.materializationId, ...input.includePrevious ? [input.projectId, input.sourceId, ...input.incremental ? [input.materializationId] : []] : []);
12991
+ SELECT DISTINCT ${REQUESTED_ENTITY}, json_extract(value, '$.rootKeys') AS root_keys
12992
+ FROM json_each(?)
12993
+ ) SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested_entities AS requested
12994
+ CROSS JOIN ontology_source_roots AS roots ON roots.version_id = ?
12995
+ AND roots.root_key IN (SELECT value FROM json_each(requested.root_keys))
12996
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12997
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}
12998
+ ${previous}`).all(canonicalJson(input.refs.map(requestedEntity)), version, ...input.includePrevious ? [input.projectId, input.sourceId, ...input.incremental ? [version] : []] : []);
12594
12999
  }
12595
13000
 
12596
13001
  // src/ontology-storage/materialization-state.ts
@@ -12803,19 +13208,13 @@ class SqliteMaterializationStateReader {
12803
13208
  json_extract(value, '$.linkId') AS link_id
12804
13209
  FROM json_each(?)
12805
13210
  )
12806
- SELECT rows.* FROM requested
13211
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
12807
13212
  CROSS JOIN ontology_source_rows AS rows
12808
- ON rows.project_id = ?
12809
- AND rows.entity_kind = 'link'
13213
+ ON rows.entity_kind = 'link'
12810
13214
  AND rows.source_type_id = requested.source_type_id
12811
13215
  AND rows.source_primary_id = requested.source_primary_id
12812
13216
  AND rows.link_id = requested.link_id
12813
- JOIN ontology_source_roots AS sources
12814
- ON sources.project_id = rows.project_id
12815
- AND sources.source_id = rows.source_id
12816
- AND sources.materialization_id = rows.materialization_id
12817
- AND sources.root_sort_key = rows.root_sort_key
12818
- WHERE sources.active = 1`).all(requestedJson, this.projectId);
13217
+ ${LIVE_SOURCE_ROWS_JOIN}`).all(requestedJson, this.projectId);
12819
13218
  const slotOverrideRows = this.db.query(`WITH requested AS (
12820
13219
  SELECT DISTINCT
12821
13220
  json_extract(value, '$.sourceTypeId') AS source_type_id,
@@ -12904,12 +13303,8 @@ class SqliteMaterializationStateReader {
12904
13303
  SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
12905
13304
  rows.target_type_id, rows.target_primary_id
12906
13305
  FROM ontology_source_rows AS rows
12907
- JOIN ontology_source_roots AS sources
12908
- ON sources.project_id = rows.project_id
12909
- AND sources.source_id = rows.source_id
12910
- AND sources.materialization_id = rows.materialization_id
12911
- AND sources.root_sort_key = rows.root_sort_key
12912
- WHERE rows.project_id = ? AND rows.entity_kind = 'link' AND sources.active = 1
13306
+ ${LIVE_SOURCE_ROWS_JOIN}
13307
+ WHERE rows.entity_kind = 'link'
12913
13308
  ), selected AS (
12914
13309
  SELECT DISTINCT links.*,
12915
13310
  ${linkSortExpression("links")} AS sort_key
@@ -13020,7 +13415,7 @@ class SqliteMaterializationStateReader {
13020
13415
  this.prepareReplacementLinks(input);
13021
13416
  }
13022
13417
  prepareReplacementObjects(input) {
13023
- const rows = replacementSourceRows({
13418
+ const rows = replacementSourceRows(this.db, {
13024
13419
  projectId: this.projectId,
13025
13420
  sourceId: input.sourceId,
13026
13421
  materializationId: input.candidateMaterializationId,
@@ -13031,14 +13426,14 @@ class SqliteMaterializationStateReader {
13031
13426
  session_id, entity_kind, identity_key, sort_key, diff_required
13032
13427
  )
13033
13428
  SELECT ?, 'object', json_array(object_type_id, primary_id),
13034
- MIN(entity_sort_key), 1
13429
+ ${objectSortExpression()}, 1
13035
13430
  FROM (${rows.sql}) AS rows
13036
13431
  WHERE entity_kind = 'object'
13037
13432
  GROUP BY object_type_id, primary_id
13038
13433
  `).run(input.sessionId, ...rows.values);
13039
13434
  }
13040
13435
  prepareReplacementLinks(input) {
13041
- const rows = replacementSourceRows({
13436
+ const rows = replacementSourceRows(this.db, {
13042
13437
  projectId: this.projectId,
13043
13438
  sourceId: input.sourceId,
13044
13439
  materializationId: input.candidateMaterializationId,
@@ -13095,31 +13490,21 @@ class SqliteMaterializationStateReader {
13095
13490
  UNION
13096
13491
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
13097
13492
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
13098
- FROM ontology_source_rows AS rows
13099
- JOIN ontology_source_roots AS sources
13100
- ON sources.project_id = rows.project_id
13101
- AND sources.source_id = rows.source_id
13102
- AND sources.materialization_id = rows.materialization_id
13103
- AND sources.root_sort_key = rows.root_sort_key
13104
- JOIN incident_objects
13105
- ON incident_objects.object_type_id = rows.source_type_id
13106
- AND incident_objects.primary_id = rows.source_primary_id
13107
- WHERE rows.project_id = ? AND rows.entity_kind = 'link'
13108
- AND sources.active = 1
13493
+ FROM incident_objects
13494
+ CROSS JOIN ontology_source_rows AS rows
13495
+ ON rows.entity_kind = 'link'
13496
+ AND rows.source_type_id = incident_objects.object_type_id
13497
+ AND rows.source_primary_id = incident_objects.primary_id
13498
+ ${LIVE_SOURCE_ROWS_JOIN}
13109
13499
  UNION
13110
13500
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
13111
13501
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
13112
- FROM ontology_source_rows AS rows
13113
- JOIN ontology_source_roots AS sources
13114
- ON sources.project_id = rows.project_id
13115
- AND sources.source_id = rows.source_id
13116
- AND sources.materialization_id = rows.materialization_id
13117
- AND sources.root_sort_key = rows.root_sort_key
13118
- JOIN incident_objects
13119
- ON incident_objects.object_type_id = rows.target_type_id
13120
- AND incident_objects.primary_id = rows.target_primary_id
13121
- WHERE rows.project_id = ? AND rows.entity_kind = 'link'
13122
- AND sources.active = 1
13502
+ FROM incident_objects
13503
+ CROSS JOIN ontology_source_rows AS rows
13504
+ ON rows.entity_kind = 'link'
13505
+ AND rows.target_type_id = incident_objects.object_type_id
13506
+ AND rows.target_primary_id = incident_objects.primary_id
13507
+ ${LIVE_SOURCE_ROWS_JOIN}
13123
13508
  ), diff_links AS (
13124
13509
  SELECT * FROM replacement_links
13125
13510
  UNION SELECT * FROM incident_links
@@ -13269,9 +13654,7 @@ function storedSource(row) {
13269
13654
  return {
13270
13655
  source: { projectionId: row.source_id },
13271
13656
  materializationId: row.materialization_id,
13272
- root: parseJson2(row.root),
13273
- assertion: parseJson2(row.assertion),
13274
- stagingOrdinal: row.staging_ordinal
13657
+ ...sourceAssertion(row)
13275
13658
  };
13276
13659
  }
13277
13660
  function effectiveObjectSnapshot(row) {
@@ -13319,6 +13702,10 @@ function storedPoint(row) {
13319
13702
  lastCommitId: row.last_commit_id
13320
13703
  };
13321
13704
  }
13705
+ function objectSortExpression(alias) {
13706
+ const prefix = alias ? `${alias}.` : "";
13707
+ return `LOWER(HEX(CAST(json_array(${prefix}object_type_id, ${prefix}primary_id) AS BLOB)))`;
13708
+ }
13322
13709
  function linkSortExpression(alias) {
13323
13710
  const prefix = alias ? `${alias}.` : "";
13324
13711
  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)))`;
@@ -14499,51 +14886,80 @@ import {
14499
14886
  sourceEntityColumns as entityColumns,
14500
14887
  isExactStagingManifest,
14501
14888
  reconcileSourceStageRows,
14889
+ sourceAssertionPayload,
14502
14890
  sourceConflict,
14503
14891
  sourceMaterializationIdentity,
14504
14892
  sourceStageRow,
14505
- sourceStageRows,
14506
- utf8SortKey as utf8SortKey2
14893
+ sourceStageRows
14507
14894
  } from "@sixb/core/internal/ontology-storage-provider";
14508
14895
 
14509
14896
  // src/ontology-storage/source-cleanup.ts
14897
+ function purgeAbandonedSourceVersions(db, input) {
14898
+ let remaining = input.limit;
14899
+ let rowsDeleted = 0;
14900
+ let materializationsDeleted = 0;
14901
+ const nextCandidate = db.query(`
14902
+ SELECT version_id FROM ontology_sources
14903
+ WHERE project_id = ? AND status = 'abandoned'
14904
+ ORDER BY terminal_at, source_id, materialization_id LIMIT 1
14905
+ `);
14906
+ const deleteRows = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
14907
+ SELECT rows.rowid FROM ontology_source_roots AS roots
14908
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
14909
+ WHERE roots.version_id = ? LIMIT ?)`);
14910
+ const deleteRoots = db.query(`DELETE FROM ontology_source_roots WHERE id IN (
14911
+ SELECT id FROM ontology_source_roots WHERE version_id = ? LIMIT ?)`);
14912
+ const deleteManifest = db.query("DELETE FROM ontology_sources WHERE version_id = ? AND status = 'abandoned'");
14913
+ while (remaining > 0) {
14914
+ const candidate = nextCandidate.get(input.projectId);
14915
+ if (!candidate)
14916
+ break;
14917
+ const rows = deleteRows.run(candidate.version_id, remaining).changes;
14918
+ rowsDeleted += rows;
14919
+ remaining -= rows;
14920
+ if (remaining === 0)
14921
+ break;
14922
+ const roots = deleteRoots.run(candidate.version_id, remaining).changes;
14923
+ rowsDeleted += roots;
14924
+ remaining -= roots;
14925
+ if (remaining === 0)
14926
+ break;
14927
+ const manifest = deleteManifest.run(candidate.version_id).changes;
14928
+ if (manifest === 0)
14929
+ break;
14930
+ materializationsDeleted += manifest;
14931
+ remaining -= manifest;
14932
+ }
14933
+ return { rowsDeleted, materializationsDeleted };
14934
+ }
14510
14935
  function cleanupSourceVersions(db, input) {
14511
14936
  let remaining = input.limit;
14937
+ const retired = `roots.project_id = ? AND roots.retired_at < ? AND versions.status = 'superseded'`;
14512
14938
  let rowsDeleted = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
14513
14939
  SELECT rows.rowid FROM ontology_source_roots AS roots
14514
- JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
14515
- CROSS JOIN ontology_source_rows AS rows
14516
- ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
14517
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
14518
- WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
14519
- AND sources.status IN ('superseded', 'abandoned')
14520
- ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
14940
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
14941
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
14942
+ WHERE ${retired}
14943
+ ORDER BY roots.retired_at, roots.id
14521
14944
  LIMIT ?
14522
14945
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14523
14946
  remaining -= rowsDeleted;
14524
14947
  if (remaining > 0) {
14525
- const removed = db.query(`DELETE FROM ontology_source_roots WHERE rowid IN (
14526
- SELECT roots.rowid FROM ontology_source_roots AS roots
14527
- JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
14528
- WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
14529
- AND sources.status IN ('superseded', 'abandoned')
14530
- AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
14531
- WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
14532
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
14533
- ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key LIMIT ?
14948
+ const removed = db.query(`DELETE FROM ontology_source_roots WHERE id IN (
14949
+ SELECT roots.id FROM ontology_source_roots AS roots
14950
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
14951
+ WHERE ${retired}
14952
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
14953
+ ORDER BY roots.retired_at, roots.id LIMIT ?
14534
14954
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14535
14955
  rowsDeleted += removed;
14536
14956
  remaining -= removed;
14537
14957
  }
14538
- const materializationsDeleted = remaining === 0 ? 0 : db.query(`DELETE FROM ontology_sources WHERE rowid IN (
14539
- SELECT sources.rowid FROM ontology_sources AS sources
14540
- WHERE project_id = ? AND status IN ('superseded', 'abandoned') AND terminal_at < ?
14958
+ const materializationsDeleted = remaining === 0 ? 0 : db.query(`DELETE FROM ontology_sources WHERE version_id IN (
14959
+ SELECT versions.version_id FROM ontology_sources AS versions
14960
+ WHERE project_id = ? AND status = 'superseded' AND terminal_at < ?
14541
14961
  AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
14542
- WHERE roots.project_id = sources.project_id AND roots.source_id = sources.source_id
14543
- AND roots.materialization_id = sources.materialization_id)
14544
- AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
14545
- WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
14546
- AND rows.materialization_id = sources.materialization_id)
14962
+ WHERE roots.version_id = versions.version_id)
14547
14963
  ORDER BY terminal_at, source_id, materialization_id LIMIT ?
14548
14964
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14549
14965
  return { rowsDeleted, materializationsDeleted };
@@ -14575,7 +14991,7 @@ class SqliteOntologySourceStorage {
14575
14991
  LIMIT 1
14576
14992
  `).get(input.projectId, input.execution.projectionRunId);
14577
14993
  if (candidate) {
14578
- throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`);
14994
+ throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; adopt it before beginning another.`);
14579
14995
  }
14580
14996
  this.db.query(`
14581
14997
  INSERT INTO ontology_sources (
@@ -14601,10 +15017,10 @@ class SqliteOntologySourceStorage {
14601
15017
  throw sourceConflict(`Source materialization '${input.materializationId}' is '${manifest.status}' and cannot accept rows.`);
14602
15018
  }
14603
15019
  const rows = sourceStageRows(manifest.projection_kind, input.rows);
14604
- const existing = this.findStageRows(input, rows);
15020
+ const existing = this.findStageRows(manifest, rows);
14605
15021
  const { pending, unchanged } = reconcileSourceStageRows(rows, existing);
14606
15022
  stageSourceRoots(this.db, manifest, input);
14607
- this.insertStageRows(input, pending);
15023
+ this.insertStageRows(manifest, pending);
14608
15024
  return { inserted: pending.length, unchanged };
14609
15025
  });
14610
15026
  await yieldSqliteEventLoop();
@@ -14654,13 +15070,40 @@ class SqliteOntologySourceStorage {
14654
15070
  return row ? sourceRecord(row) : null;
14655
15071
  });
14656
15072
  }
15073
+ async adopt(input) {
15074
+ return this.runRootOperation(() => {
15075
+ assertProjectAndSource(input);
15076
+ assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
15077
+ assertTimestamp(input.adoptedAt, "Source adoptedAt", true);
15078
+ this.assertExecution(input);
15079
+ const candidate = this.findRunCandidate(input);
15080
+ if (!candidate)
15081
+ return null;
15082
+ const changed = this.db.query(`
15083
+ UPDATE ontology_sources SET execution_token = ?, updated_at = MAX(updated_at, ?)
15084
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?
15085
+ AND status IN ('staging', 'ready')
15086
+ `).run(input.execution.executionToken, input.adoptedAt, candidate.project_id, candidate.source_id, candidate.materialization_id).changes;
15087
+ if (changed !== 1) {
15088
+ throw sourceConflict(`Source materialization '${candidate.materialization_id}' changed.`);
15089
+ }
15090
+ const adopted = this.requireManifest(candidate.project_id, candidate.source_id, candidate.materialization_id);
15091
+ return {
15092
+ record: sourceRecord(adopted),
15093
+ resumeStagingOrdinal: adopted.status === "ready" ? Number(adopted.root_count) : this.lastStagedOrdinal(adopted)
15094
+ };
15095
+ });
15096
+ }
14657
15097
  async abandon(input) {
14658
15098
  return this.runRootOperation(() => {
14659
15099
  assertProjectAndSource(input);
14660
15100
  assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
14661
15101
  assertTimestamp(input.abandonedAt, "Source abandonedAt", true);
14662
15102
  this.assertExecution(input);
14663
- return input.kind === "candidate" ? this.abandonCandidate(input) : this.abandonForReclaim(input);
15103
+ if (input.kind === "candidate")
15104
+ return this.abandonCandidate(input);
15105
+ const candidate = this.findRunCandidate(input);
15106
+ return candidate ? this.transitionToAbandoned(candidate, input.abandonedAt) : null;
14664
15107
  });
14665
15108
  }
14666
15109
  async cleanupTerminal(input) {
@@ -14671,6 +15114,13 @@ class SqliteOntologySourceStorage {
14671
15114
  return cleanupSourceVersions(this.db, input);
14672
15115
  });
14673
15116
  }
15117
+ async purgeAbandoned(input) {
15118
+ return this.runRootOperation(() => {
15119
+ assertNonblank(input.projectId, "Abandoned source purge project id");
15120
+ assertPositiveInteger(input.limit, "Abandoned source purge limit");
15121
+ return purgeAbandonedSourceVersions(this.db, input);
15122
+ });
15123
+ }
14674
15124
  async summarizeTerminal(input) {
14675
15125
  return this.runRootOperation(() => {
14676
15126
  assertNonblank(input.projectId, "Terminal source summary project id");
@@ -14696,19 +15146,20 @@ class SqliteOntologySourceStorage {
14696
15146
  assertSourceCandidateOwner(sourceRecord(manifest), input.execution);
14697
15147
  return this.transitionToAbandoned(manifest, input.abandonedAt);
14698
15148
  }
14699
- abandonForReclaim(input) {
15149
+ findRunCandidate(input) {
14700
15150
  const candidates = this.db.query(`
14701
15151
  SELECT * FROM ontology_sources
14702
15152
  WHERE project_id = ? AND source_id = ? AND projection_run_id = ?
14703
15153
  AND status IN ('staging', 'ready')
14704
15154
  `).all(input.projectId, input.source.projectionId, input.execution.projectionRunId);
14705
- if (candidates.some((candidate) => candidate.execution_token === input.execution.executionToken)) {
14706
- throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`);
14707
- }
14708
15155
  if (candidates.length > 1) {
14709
15156
  throw sourceConflict(`Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`);
14710
15157
  }
14711
- return candidates[0] ? this.transitionToAbandoned(candidates[0], input.abandonedAt) : null;
15158
+ return candidates[0] ?? null;
15159
+ }
15160
+ lastStagedOrdinal(manifest) {
15161
+ const last = this.db.query("SELECT MAX(staging_ordinal) AS ordinal FROM ontology_source_roots WHERE version_id = ?").get(manifest.version_id);
15162
+ return last?.ordinal ?? 0;
14712
15163
  }
14713
15164
  transitionToAbandoned(manifest, abandonedAt) {
14714
15165
  if (abandonedAt < manifest.created_at || manifest.ready_at !== null && abandonedAt < manifest.ready_at) {
@@ -14723,84 +15174,95 @@ class SqliteOntologySourceStorage {
14723
15174
  if (changed !== 1) {
14724
15175
  throw sourceConflict(`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`);
14725
15176
  }
14726
- this.db.query(`UPDATE ontology_source_roots SET retired_at = ?
14727
- WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND active = 0`).run(abandonedAt, manifest.project_id, manifest.source_id, manifest.materialization_id);
14728
15177
  return sourceRecord(this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id));
14729
15178
  }
14730
15179
  assertReadyState(manifest, rootCount, assertionCount) {
14731
15180
  const counts = this.db.query(`
14732
- SELECT (SELECT COUNT(*) FROM ontology_source_rows
14733
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?) AS assertions, COUNT(*) AS roots,
14734
- COUNT(DISTINCT staging_ordinal) AS ordinals,
15181
+ SELECT (SELECT COUNT(*) FROM ontology_source_roots AS roots
15182
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15183
+ WHERE roots.version_id = ?1) AS assertions,
15184
+ COUNT(*) AS roots, COUNT(DISTINCT staging_ordinal) AS ordinals,
14735
15185
  MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
14736
15186
  FROM ontology_source_roots
14737
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14738
- `).get(manifest.project_id, manifest.source_id, manifest.materialization_id, manifest.project_id, manifest.source_id, manifest.materialization_id);
15187
+ WHERE version_id = ?1
15188
+ `).get(manifest.version_id);
14739
15189
  if (counts.assertions !== assertionCount || counts.roots !== rootCount || counts.ordinals !== rootCount || rootCount > 0 && (counts.min_ordinal !== 0 || counts.max_ordinal !== rootCount - 1)) {
14740
15190
  throw new MaterializationValidationError4("Source ready counts do not match the staged roots and assertions.");
14741
15191
  }
14742
15192
  assertSourceRootCoverage(this.db, manifest);
14743
15193
  const invalid = this.db.query(manifest.projection_kind === "link" ? `
14744
- SELECT root_key
14745
- FROM ontology_source_rows
14746
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14747
- GROUP BY root_key
14748
- HAVING COUNT(*) <> 1 OR MIN(root_kind) <> 'link' OR MIN(entity_kind) <> 'link'
14749
- OR MIN(root_key) <> MIN(entity_key)
15194
+ SELECT roots.root_key
15195
+ FROM ontology_source_roots AS roots
15196
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15197
+ WHERE roots.version_id = ?
15198
+ GROUP BY roots.id
15199
+ HAVING COUNT(*) <> 1
15200
+ OR MAX(json_extract(roots.root_key, '$[0]') <> 'link')
15201
+ OR MAX(rows.entity_kind <> 'link')
15202
+ OR MAX(rows.source_type_id <> json_extract(roots.root_key, '$[1]')
15203
+ OR rows.source_primary_id <> json_extract(roots.root_key, '$[2]')
15204
+ OR rows.link_id <> json_extract(roots.root_key, '$[3]')
15205
+ OR rows.target_type_id <> json_extract(roots.root_key, '$[4]')
15206
+ OR rows.target_primary_id <> json_extract(roots.root_key, '$[5]'))
14750
15207
  LIMIT 1
14751
15208
  ` : `
14752
- SELECT root_key
14753
- FROM ontology_source_rows
14754
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14755
- GROUP BY root_key
14756
- HAVING MIN(root_kind) <> 'object'
14757
- OR SUM(CASE WHEN entity_kind = 'object' AND entity_key = root_key THEN 1 ELSE 0 END) <> 1
14758
- OR SUM(CASE
14759
- WHEN entity_kind = 'object' AND entity_key <> root_key THEN 1
14760
- WHEN entity_kind = 'link' AND (
14761
- source_type_id <> root_object_type_id OR source_primary_id <> root_primary_id
14762
- ) THEN 1
14763
- ELSE 0
14764
- END) <> 0
15209
+ SELECT roots.root_key
15210
+ FROM ontology_source_roots AS roots
15211
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15212
+ WHERE roots.version_id = ?
15213
+ GROUP BY roots.id
15214
+ HAVING MAX(json_extract(roots.root_key, '$[0]') <> 'object')
15215
+ OR SUM(rows.entity_kind = 'object'
15216
+ AND rows.object_type_id = json_extract(roots.root_key, '$[1]')
15217
+ AND rows.primary_id = json_extract(roots.root_key, '$[2]')) <> 1
15218
+ OR SUM(
15219
+ (rows.entity_kind = 'object' AND (
15220
+ rows.object_type_id <> json_extract(roots.root_key, '$[1]')
15221
+ OR rows.primary_id <> json_extract(roots.root_key, '$[2]')))
15222
+ OR (rows.entity_kind = 'link' AND (
15223
+ rows.source_type_id <> json_extract(roots.root_key, '$[1]')
15224
+ OR rows.source_primary_id <> json_extract(roots.root_key, '$[2]')))
15225
+ ) <> 0
15226
+ OR SUM(rows.entity_kind = 'link') <> COUNT(DISTINCT CASE
15227
+ WHEN rows.entity_kind = 'link'
15228
+ THEN json_array(rows.link_id, rows.target_type_id, rows.target_primary_id)
15229
+ END)
14765
15230
  LIMIT 1
14766
- `).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
15231
+ `).get(manifest.version_id);
14767
15232
  if (invalid) {
14768
15233
  throw new MaterializationValidationError4(manifest.projection_kind === "link" ? "Link projection roots must contain exactly their matching link assertion." : "Object projection roots must contain exactly their matching object assertion plus links sourced from that root.");
14769
15234
  }
14770
15235
  }
14771
- findStageRows(input, rows) {
15236
+ findStageRows(manifest, rows) {
14772
15237
  if (rows.length === 0)
14773
15238
  return [];
14774
- const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.rootKey)))];
15239
+ const rootKeys = [...new Set(rows.map((row) => row.rootKey))];
14775
15240
  const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))];
14776
- const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.entityKey)))];
14777
15241
  const existing = this.db.query(`
14778
- SELECT * FROM ontology_source_rows
14779
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14780
- AND (
14781
- root_sort_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))
14782
- OR staging_ordinal IN (SELECT CAST(value AS INTEGER) FROM json_each(?))
14783
- OR entity_sort_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))
14784
- )
14785
- `).all(input.projectId, input.source.projectionId, input.materializationId, JSON.stringify(rootSortKeys), JSON.stringify(ordinals), JSON.stringify(entitySortKeys));
15242
+ WITH selected AS (
15243
+ SELECT roots.id FROM json_each(?2) AS requested
15244
+ CROSS JOIN ontology_source_roots AS roots
15245
+ ON roots.version_id = ?1 AND roots.root_key = CAST(requested.value AS TEXT)
15246
+ UNION
15247
+ SELECT roots.id FROM json_each(?3) AS requested
15248
+ CROSS JOIN ontology_source_roots AS roots
15249
+ ON roots.version_id = ?1 AND roots.staging_ordinal = CAST(requested.value AS INTEGER)
15250
+ )
15251
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM selected
15252
+ CROSS JOIN ontology_source_roots AS roots ON roots.id = selected.id
15253
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
15254
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15255
+ `).all(manifest.version_id, JSON.stringify(rootKeys), JSON.stringify(ordinals));
14786
15256
  return existing.map((row) => sourceStageRow(sourceAssertion(row)));
14787
15257
  }
14788
- insertStageRows(input, rows) {
15258
+ insertStageRows(manifest, rows) {
14789
15259
  if (rows.length === 0)
14790
15260
  return;
14791
- const payload = rows.map(({ entityKey, rootKey, row }) => {
15261
+ const payload = rows.map(({ rootKey, row }) => {
14792
15262
  const entity = entityColumns(row.assertion);
14793
- const root = entityColumns(row.root);
14794
15263
  return {
14795
- entityKind: row.assertion.kind,
14796
- entityKey,
14797
- entitySortKey: utf8SortKey2(entityKey),
14798
- rootKind: row.root.kind,
14799
15264
  rootKey,
14800
- rootSortKey: utf8SortKey2(rootKey),
14801
- stagingOrdinal: row.stagingOrdinal,
14802
- root: row.root,
14803
- assertion: row.assertion,
15265
+ entityKind: row.assertion.kind,
14804
15266
  objectTypeId: entity.objectTypeId,
14805
15267
  primaryId: entity.primaryId,
14806
15268
  sourceTypeId: entity.sourceTypeId,
@@ -14808,45 +15270,27 @@ class SqliteOntologySourceStorage {
14808
15270
  linkId: entity.linkId,
14809
15271
  targetTypeId: entity.targetTypeId,
14810
15272
  targetPrimaryId: entity.targetPrimaryId,
14811
- rootObjectTypeId: root.objectTypeId,
14812
- rootPrimaryId: root.primaryId,
14813
- rootSourceTypeId: root.sourceTypeId,
14814
- rootSourcePrimaryId: root.sourcePrimaryId,
14815
- rootLinkId: root.linkId,
14816
- rootTargetTypeId: root.targetTypeId,
14817
- rootTargetPrimaryId: root.targetPrimaryId
15273
+ payload: sourceAssertionPayload(row.assertion)
14818
15274
  };
14819
15275
  });
14820
- this.db.query(`
15276
+ const inserted = this.db.query(`
14821
15277
  WITH staged(value) AS (SELECT value FROM json_each(?))
14822
15278
  INSERT INTO ontology_source_rows (
14823
- project_id, source_id, materialization_id,
14824
- entity_kind, entity_key, entity_sort_key,
14825
- root_kind, root_key, root_sort_key, staging_ordinal,
14826
- root, assertion,
14827
- object_type_id, primary_id,
14828
- source_type_id, source_primary_id, link_id, target_type_id, target_primary_id,
14829
- root_object_type_id, root_primary_id,
14830
- root_source_type_id, root_source_primary_id, root_link_id,
14831
- root_target_type_id, root_target_primary_id
15279
+ root_id, entity_kind, object_type_id, primary_id,
15280
+ source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
14832
15281
  )
14833
- SELECT
14834
- ?, ?, ?,
14835
- json_extract(value, '$.entityKind'), json_extract(value, '$.entityKey'),
14836
- json_extract(value, '$.entitySortKey'),
14837
- json_extract(value, '$.rootKind'), json_extract(value, '$.rootKey'),
14838
- json_extract(value, '$.rootSortKey'), json_extract(value, '$.stagingOrdinal'),
14839
- json(json_extract(value, '$.root')), json(json_extract(value, '$.assertion')),
15282
+ SELECT roots.id, json_extract(value, '$.entityKind'),
14840
15283
  json_extract(value, '$.objectTypeId'), json_extract(value, '$.primaryId'),
14841
15284
  json_extract(value, '$.sourceTypeId'), json_extract(value, '$.sourcePrimaryId'),
14842
15285
  json_extract(value, '$.linkId'), json_extract(value, '$.targetTypeId'),
14843
- json_extract(value, '$.targetPrimaryId'),
14844
- json_extract(value, '$.rootObjectTypeId'), json_extract(value, '$.rootPrimaryId'),
14845
- json_extract(value, '$.rootSourceTypeId'),
14846
- json_extract(value, '$.rootSourcePrimaryId'), json_extract(value, '$.rootLinkId'),
14847
- json_extract(value, '$.rootTargetTypeId'), json_extract(value, '$.rootTargetPrimaryId')
15286
+ json_extract(value, '$.targetPrimaryId'), json_extract(value, '$.payload')
14848
15287
  FROM staged
14849
- `).run(canonicalJson(payload), input.projectId, input.source.projectionId, input.materializationId);
15288
+ CROSS JOIN ontology_source_roots AS roots
15289
+ ON roots.version_id = ? AND roots.root_key = json_extract(value, '$.rootKey')
15290
+ `).run(canonicalJson(payload), manifest.version_id).changes;
15291
+ if (inserted !== rows.length) {
15292
+ throw sourceConflict("Staged source rows do not all belong to a staged root.");
15293
+ }
14850
15294
  }
14851
15295
  assertExecution(input, identity) {
14852
15296
  assertProjectionExecution(this.db, {
@@ -15113,6 +15557,8 @@ function queryLatestRunsByOwnerId(input) {
15113
15557
  return [];
15114
15558
  }
15115
15559
  const placeholders = ownerIds.map(() => "?").join(", ");
15560
+ const statusFilter = input.excludedStatus === undefined ? "" : " AND status <> ?";
15561
+ const statusArgs = input.excludedStatus === undefined ? [] : [input.excludedStatus];
15116
15562
  const orderColumn = input.tableName === "sync_runs" || input.tableName === "pipeline_runs" || input.tableName === "projection_runs" ? "COALESCE(started_at, queued_at)" : "started_at";
15117
15563
  const rows = input.db.query(`
15118
15564
  SELECT *
@@ -15124,10 +15570,10 @@ function queryLatestRunsByOwnerId(input) {
15124
15570
  ORDER BY ${orderColumn} DESC, id DESC
15125
15571
  ) AS sixb_latest_rank
15126
15572
  FROM ${input.tableName}
15127
- WHERE project_id = ? AND ${input.ownerColumn} IN (${placeholders})
15573
+ WHERE project_id = ? AND ${input.ownerColumn} IN (${placeholders})${statusFilter}
15128
15574
  )
15129
15575
  WHERE sixb_latest_rank = 1
15130
- `).all(input.projectId, ...ownerIds);
15576
+ `).all(input.projectId, ...ownerIds, ...statusArgs);
15131
15577
  const latestByOwnerId = new Map(rows.map((row) => [input.ownerIdFor(row), row]));
15132
15578
  return ownerIds.flatMap((ownerId) => {
15133
15579
  const row = latestByOwnerId.get(ownerId);
@@ -15473,12 +15919,14 @@ import {
15473
15919
  failProjectionRunEnqueue,
15474
15920
  immutableDatasetVersionConflict,
15475
15921
  mergeProjectionRunProgress,
15922
+ parseProjectionAttemptFailure,
15476
15923
  planProjectionRunFinish,
15477
15924
  planProjectionRunReclaim,
15478
15925
  projectionRunNotFound,
15479
15926
  publicProjectionRunRecord,
15480
15927
  requireTelemetryProjectionRun,
15481
15928
  restoreProjectionRun,
15929
+ SUPERSEDING_PROJECTION_RUN_STATUSES,
15482
15930
  staleProjectionRunExecution
15483
15931
  } from "@sixb/core/internal/projection-run-storage-provider";
15484
15932
  import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage";
@@ -15560,7 +16008,7 @@ class SqliteProjectionRunStorage {
15560
16008
  const result = this.db.query(`
15561
16009
  UPDATE projection_runs
15562
16010
  SET status = 'running', started_at = COALESCE(started_at, ?), finished_at = NULL,
15563
- attempt = ?, execution_token = ?, error = NULL
16011
+ attempt = ?, execution_token = ?
15564
16012
  WHERE project_id = ? AND id = ?
15565
16013
  AND (
15566
16014
  (status = 'queued' AND execution_token IS NULL)
@@ -15604,6 +16052,20 @@ class SqliteProjectionRunStorage {
15604
16052
  return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id));
15605
16053
  });
15606
16054
  }
16055
+ async recordAttemptFailure(input) {
16056
+ return runImmediateTransaction(this.db, () => {
16057
+ const existingRow = this.requireMaterializationExecution(input);
16058
+ const error = parseProjectionAttemptFailure(restoreProjectionRunRow(existingRow), input.error);
16059
+ const result = this.db.query(`
16060
+ UPDATE projection_runs
16061
+ SET error = ?
16062
+ WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
16063
+ `).run(serializeSixbFailure4(error, PROJECTION_RUN_FAILURE_CODES), input.projectId, input.id, input.executionToken);
16064
+ if (result.changes !== 1)
16065
+ throw staleProjectionRunExecution(input.id);
16066
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id));
16067
+ });
16068
+ }
15607
16069
  async finish(input) {
15608
16070
  return runImmediateTransaction(this.db, () => {
15609
16071
  const existingRow = this.requireMaterializationExecution(input);
@@ -15741,6 +16203,21 @@ class SqliteProjectionRunStorage {
15741
16203
  const runs = rows.map(rowToProjectionRunRecord);
15742
16204
  return { runs, hasMore: offset + runs.length < totalRow.count, total: totalRow.count };
15743
16205
  }
16206
+ async findSupersedingRun(input) {
16207
+ const run = this.requireRow(input.projectId, input.id);
16208
+ if (run.materialization_protocol !== "replacement")
16209
+ return null;
16210
+ const statuses = SUPERSEDING_PROJECTION_RUN_STATUSES;
16211
+ const newer = this.db.query(`
16212
+ SELECT * FROM projection_runs
16213
+ WHERE project_id = ? AND projection_id = ? AND materialization_protocol = 'replacement'
16214
+ AND status IN (${statuses.map(() => "?").join(", ")})
16215
+ AND dataset_version_created_at > ?
16216
+ ORDER BY dataset_version_created_at DESC, id DESC
16217
+ LIMIT 1
16218
+ `).get(input.projectId, run.projection_id, ...statuses, run.dataset_version_created_at);
16219
+ return newer ? rowToProjectionRunRecord(newer) : null;
16220
+ }
15744
16221
  async listLatestByProjectionIds(input) {
15745
16222
  const rows = queryLatestRunsByOwnerId({
15746
16223
  db: this.db,
@@ -15748,7 +16225,8 @@ class SqliteProjectionRunStorage {
15748
16225
  ownerColumn: "projection_id",
15749
16226
  ownerIds: input.projectionIds,
15750
16227
  projectId: input.projectId,
15751
- ownerIdFor: (row) => row.projection_id
16228
+ ownerIdFor: (row) => row.projection_id,
16229
+ excludedStatus: "superseded"
15752
16230
  });
15753
16231
  return { runs: rows.map(rowToProjectionRunRecord) };
15754
16232
  }
@@ -17945,4 +18423,4 @@ export {
17945
18423
  migrateSqliteStorage
17946
18424
  };
17947
18425
 
17948
- //# debugId=9F25188860B63C3464756E2164756E21
18426
+ //# debugId=A2CEAFAB428734DF64756E2164756E21