@sixb/sqlite 0.1.14 → 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.
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
  }
@@ -12204,6 +12531,10 @@ function assertReconciliationPageLimit(limit) {
12204
12531
  // src/ontology-storage/shared.ts
12205
12532
  import { parseSixbFailure as parseSixbFailure2 } from "@sixb/core/internal/errors";
12206
12533
  import { MaterializationConflictError } from "@sixb/core/internal/materialization";
12534
+ import {
12535
+ sourceAssertionFromColumns,
12536
+ sourceEntityFromKey
12537
+ } from "@sixb/core/internal/ontology-storage-provider";
12207
12538
  import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage";
12208
12539
  import {
12209
12540
  assertNonblank,
@@ -12256,8 +12587,8 @@ function sourceRecord(row) {
12256
12587
  }
12257
12588
  function sourceAssertion(row) {
12258
12589
  return {
12259
- root: parseJson2(row.root),
12260
- assertion: parseJson2(row.assertion),
12590
+ root: sourceEntityFromKey(row.root_key),
12591
+ assertion: sourceAssertionFromColumns(row, row.payload === null ? null : parseJson2(row.payload)),
12261
12592
  stagingOrdinal: row.staging_ordinal
12262
12593
  };
12263
12594
  }
@@ -12475,29 +12806,68 @@ import {
12475
12806
  MaterializationValidationError,
12476
12807
  projectionEntityKey
12477
12808
  } 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";
12809
+ import {
12810
+ sourceEntityColumns,
12811
+ sourceStageRoots
12812
+ } from "@sixb/core/internal/ontology-storage-provider";
12813
+ var PUBLISHED = "('active', 'superseded')";
12814
+ var SOURCE_ASSERTION_COLUMNS = `versions.source_id, versions.materialization_id,
12815
+ roots.root_key, roots.staging_ordinal, rows.entity_kind, rows.object_type_id, rows.primary_id,
12816
+ rows.source_type_id, rows.source_primary_id, rows.link_id, rows.target_type_id,
12817
+ rows.target_primary_id, rows.payload`;
12818
+ var LIVE_SOURCE_ROWS_JOIN = `
12819
+ CROSS JOIN ontology_source_roots AS roots ON roots.id = rows.root_id
12820
+ AND roots.project_id = ? AND roots.retired_at IS NULL AND roots.deleted = 0
12821
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12822
+ AND versions.status IN ${PUBLISHED}`;
12823
+ var ENTITY_MATCH = `rows.entity_kind = requested.entity_kind
12824
+ AND rows.object_type_id IS requested.object_type_id
12825
+ AND rows.primary_id IS requested.primary_id
12826
+ AND rows.source_type_id IS requested.source_type_id
12827
+ AND rows.source_primary_id IS requested.source_primary_id
12828
+ AND rows.link_id IS requested.link_id
12829
+ AND rows.target_type_id IS requested.target_type_id
12830
+ AND rows.target_primary_id IS requested.target_primary_id`;
12831
+ var REQUESTED_ENTITY = `json_extract(value, '$.entityKind') AS entity_kind,
12832
+ json_extract(value, '$.objectTypeId') AS object_type_id,
12833
+ json_extract(value, '$.primaryId') AS primary_id,
12834
+ json_extract(value, '$.sourceTypeId') AS source_type_id,
12835
+ json_extract(value, '$.sourcePrimaryId') AS source_primary_id,
12836
+ json_extract(value, '$.linkId') AS link_id,
12837
+ json_extract(value, '$.targetTypeId') AS target_type_id,
12838
+ json_extract(value, '$.targetPrimaryId') AS target_primary_id`;
12839
+ function requestedEntity(ref) {
12840
+ const key = projectionEntityKey(ref);
12484
12841
  return {
12485
- sql: `SELECT * FROM ontology_source_rows WHERE project_id = ? AND source_id = ? AND materialization_id = ?
12842
+ entityKind: ref.kind,
12843
+ ...sourceEntityColumns(ref),
12844
+ rootKeys: ref.kind === "object" ? [key] : [key, projectionEntityKey({ kind: "object", ref: ref.ref.source })]
12845
+ };
12846
+ }
12847
+ function sourceVersionId(db, input) {
12848
+ const version = db.query(`SELECT version_id FROM ontology_sources
12849
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?`).get(input.projectId, input.sourceId, input.materializationId);
12850
+ return version?.version_id ?? null;
12851
+ }
12852
+ function replacementSourceRows(db, input) {
12853
+ const version = sourceVersionId(db, input);
12854
+ const live = `live.retired_at IS NULL AND live.deleted = 0
12855
+ AND versions.source_id = ? AND versions.status IN ${PUBLISHED}`;
12856
+ const replaced = input.incremental ? `SELECT live.id FROM ontology_source_roots AS changed
12857
+ CROSS JOIN ontology_source_roots AS live
12858
+ ON live.project_id = ? AND live.root_key = changed.root_key
12859
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = live.version_id
12860
+ WHERE changed.version_id = ? AND ${live}` : `SELECT live.id FROM ontology_sources AS versions
12861
+ CROSS JOIN ontology_source_roots AS live ON live.version_id = versions.version_id
12862
+ WHERE versions.project_id = ? AND ${live}`;
12863
+ return {
12864
+ sql: `SELECT rows.* FROM ontology_source_roots AS roots
12865
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
12866
+ WHERE roots.version_id = ?
12486
12867
  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
- ]
12868
+ SELECT rows.* FROM (${replaced}) AS roots
12869
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id`,
12870
+ values: [version, input.projectId, ...input.incremental ? [version] : [], input.sourceId]
12501
12871
  };
12502
12872
  }
12503
12873
  function stageSourceRoots(db, manifest, input) {
@@ -12506,15 +12876,15 @@ function stageSourceRoots(db, manifest, input) {
12506
12876
  throw new MaterializationValidationError("Root deletions require a source delta base.");
12507
12877
  }
12508
12878
  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 = ?`);
12879
+ WHERE version_id = ? AND root_key = ?`);
12510
12880
  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`);
12881
+ version_id, project_id, root_key, staging_ordinal, deleted
12882
+ ) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING id`);
12513
12883
  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));
12884
+ const inserted = insert.get(manifest.version_id, input.projectId, root.rootKey, root.stagingOrdinal, Number(root.deleted));
12515
12885
  if (inserted)
12516
12886
  continue;
12517
- const previous = existing.get(input.projectId, input.source.projectionId, input.materializationId, root.sortKey);
12887
+ const previous = existing.get(manifest.version_id, root.rootKey);
12518
12888
  if (!previous || previous.staging_ordinal !== root.stagingOrdinal || Boolean(previous.deleted) !== root.deleted) {
12519
12889
  throw new MaterializationValidationError("Source materialization repeats root or stream ordinal with different content.");
12520
12890
  }
@@ -12525,72 +12895,61 @@ function activateSourceRoots(db, projectId, candidate, activation) {
12525
12895
  throw new MaterializationConflictError2("projection-fence", "Source delta base changed before activation.");
12526
12896
  if (candidate.base_materialization_id !== null && candidate.root_count === 0)
12527
12897
  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);
12898
+ const delta = candidate.base_materialization_id !== null;
12899
+ db.query(`UPDATE ontology_source_roots SET retired_at = ?
12900
+ WHERE retired_at IS NULL AND deleted = 0
12901
+ AND version_id IN (SELECT version_id FROM ontology_sources
12902
+ WHERE project_id = ? AND source_id = ? AND status IN ${PUBLISHED})
12903
+ ${delta ? `AND project_id = ? AND root_key IN (
12904
+ SELECT root_key FROM ontology_source_roots WHERE version_id = ?)` : ""}`).run(activation.updatedAt, projectId, activation.source.projectionId, ...delta ? [projectId, candidate.version_id] : []);
12905
+ db.query(`UPDATE ontology_source_roots SET retired_at = ?
12906
+ WHERE version_id = ? AND deleted = 1`).run(activation.updatedAt, candidate.version_id);
12537
12907
  }
12538
12908
  function assertSourceRootCoverage(db, manifest) {
12539
12909
  const invalid = db.query(`SELECT 1 FROM ontology_source_roots AS roots
12540
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
12910
+ WHERE roots.version_id = ?
12541
12911
  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);
12912
+ SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id
12913
+ ) LIMIT 1`).get(manifest.version_id);
12546
12914
  if (invalid)
12547
12915
  throw new MaterializationValidationError("Source roots must have complete assertions or an explicit deletion, never both.");
12548
12916
  }
12549
12917
  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 }));
12918
+ const requested = refs.flatMap((ref) => {
12919
+ const { rootKeys, ...entity } = requestedEntity(ref);
12920
+ return rootKeys.map((rootKey) => ({ ...entity, rootKey }));
12557
12921
  });
12558
12922
  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
12923
+ SELECT DISTINCT json_extract(value, '$.rootKey') AS root_key, ${REQUESTED_ENTITY}
12924
+ FROM json_each(?)
12925
+ ) SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
12562
12926
  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);
12927
+ AND roots.root_key = requested.root_key AND roots.retired_at IS NULL AND roots.deleted = 0
12928
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12929
+ AND versions.status IN ${PUBLISHED}
12930
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}`).all(canonicalJson(requested), projectId);
12567
12931
  }
12568
12932
  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
- }));
12933
+ const version = sourceVersionId(db, input);
12577
12934
  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
12935
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested_entities AS requested
12936
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
12937
+ AND roots.root_key IN (SELECT value FROM json_each(requested.root_keys))
12938
+ AND roots.retired_at IS NULL AND roots.deleted = 0
12939
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12940
+ AND versions.source_id = ? AND versions.status IN ${PUBLISHED}
12941
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}
12584
12942
  ${!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)`}`;
12943
+ WHERE changed.version_id = ? AND changed.root_key = roots.root_key)`}`;
12587
12944
  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] : []] : []);
12945
+ SELECT DISTINCT ${REQUESTED_ENTITY}, json_extract(value, '$.rootKeys') AS root_keys
12946
+ FROM json_each(?)
12947
+ ) SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested_entities AS requested
12948
+ CROSS JOIN ontology_source_roots AS roots ON roots.version_id = ?
12949
+ AND roots.root_key IN (SELECT value FROM json_each(requested.root_keys))
12950
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
12951
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id AND ${ENTITY_MATCH}
12952
+ ${previous}`).all(canonicalJson(input.refs.map(requestedEntity)), version, ...input.includePrevious ? [input.projectId, input.sourceId, ...input.incremental ? [version] : []] : []);
12594
12953
  }
12595
12954
 
12596
12955
  // src/ontology-storage/materialization-state.ts
@@ -12803,19 +13162,13 @@ class SqliteMaterializationStateReader {
12803
13162
  json_extract(value, '$.linkId') AS link_id
12804
13163
  FROM json_each(?)
12805
13164
  )
12806
- SELECT rows.* FROM requested
13165
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
12807
13166
  CROSS JOIN ontology_source_rows AS rows
12808
- ON rows.project_id = ?
12809
- AND rows.entity_kind = 'link'
13167
+ ON rows.entity_kind = 'link'
12810
13168
  AND rows.source_type_id = requested.source_type_id
12811
13169
  AND rows.source_primary_id = requested.source_primary_id
12812
13170
  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);
13171
+ ${LIVE_SOURCE_ROWS_JOIN}`).all(requestedJson, this.projectId);
12819
13172
  const slotOverrideRows = this.db.query(`WITH requested AS (
12820
13173
  SELECT DISTINCT
12821
13174
  json_extract(value, '$.sourceTypeId') AS source_type_id,
@@ -12904,12 +13257,8 @@ class SqliteMaterializationStateReader {
12904
13257
  SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
12905
13258
  rows.target_type_id, rows.target_primary_id
12906
13259
  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
13260
+ ${LIVE_SOURCE_ROWS_JOIN}
13261
+ WHERE rows.entity_kind = 'link'
12913
13262
  ), selected AS (
12914
13263
  SELECT DISTINCT links.*,
12915
13264
  ${linkSortExpression("links")} AS sort_key
@@ -13020,7 +13369,7 @@ class SqliteMaterializationStateReader {
13020
13369
  this.prepareReplacementLinks(input);
13021
13370
  }
13022
13371
  prepareReplacementObjects(input) {
13023
- const rows = replacementSourceRows({
13372
+ const rows = replacementSourceRows(this.db, {
13024
13373
  projectId: this.projectId,
13025
13374
  sourceId: input.sourceId,
13026
13375
  materializationId: input.candidateMaterializationId,
@@ -13031,14 +13380,14 @@ class SqliteMaterializationStateReader {
13031
13380
  session_id, entity_kind, identity_key, sort_key, diff_required
13032
13381
  )
13033
13382
  SELECT ?, 'object', json_array(object_type_id, primary_id),
13034
- MIN(entity_sort_key), 1
13383
+ ${objectSortExpression()}, 1
13035
13384
  FROM (${rows.sql}) AS rows
13036
13385
  WHERE entity_kind = 'object'
13037
13386
  GROUP BY object_type_id, primary_id
13038
13387
  `).run(input.sessionId, ...rows.values);
13039
13388
  }
13040
13389
  prepareReplacementLinks(input) {
13041
- const rows = replacementSourceRows({
13390
+ const rows = replacementSourceRows(this.db, {
13042
13391
  projectId: this.projectId,
13043
13392
  sourceId: input.sourceId,
13044
13393
  materializationId: input.candidateMaterializationId,
@@ -13095,31 +13444,21 @@ class SqliteMaterializationStateReader {
13095
13444
  UNION
13096
13445
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
13097
13446
  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
13447
+ FROM incident_objects
13448
+ CROSS JOIN ontology_source_rows AS rows
13449
+ ON rows.entity_kind = 'link'
13450
+ AND rows.source_type_id = incident_objects.object_type_id
13451
+ AND rows.source_primary_id = incident_objects.primary_id
13452
+ ${LIVE_SOURCE_ROWS_JOIN}
13109
13453
  UNION
13110
13454
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
13111
13455
  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
13456
+ FROM incident_objects
13457
+ CROSS JOIN ontology_source_rows AS rows
13458
+ ON rows.entity_kind = 'link'
13459
+ AND rows.target_type_id = incident_objects.object_type_id
13460
+ AND rows.target_primary_id = incident_objects.primary_id
13461
+ ${LIVE_SOURCE_ROWS_JOIN}
13123
13462
  ), diff_links AS (
13124
13463
  SELECT * FROM replacement_links
13125
13464
  UNION SELECT * FROM incident_links
@@ -13269,9 +13608,7 @@ function storedSource(row) {
13269
13608
  return {
13270
13609
  source: { projectionId: row.source_id },
13271
13610
  materializationId: row.materialization_id,
13272
- root: parseJson2(row.root),
13273
- assertion: parseJson2(row.assertion),
13274
- stagingOrdinal: row.staging_ordinal
13611
+ ...sourceAssertion(row)
13275
13612
  };
13276
13613
  }
13277
13614
  function effectiveObjectSnapshot(row) {
@@ -13319,6 +13656,10 @@ function storedPoint(row) {
13319
13656
  lastCommitId: row.last_commit_id
13320
13657
  };
13321
13658
  }
13659
+ function objectSortExpression(alias) {
13660
+ const prefix = alias ? `${alias}.` : "";
13661
+ return `LOWER(HEX(CAST(json_array(${prefix}object_type_id, ${prefix}primary_id) AS BLOB)))`;
13662
+ }
13322
13663
  function linkSortExpression(alias) {
13323
13664
  const prefix = alias ? `${alias}.` : "";
13324
13665
  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 +14840,80 @@ import {
14499
14840
  sourceEntityColumns as entityColumns,
14500
14841
  isExactStagingManifest,
14501
14842
  reconcileSourceStageRows,
14843
+ sourceAssertionPayload,
14502
14844
  sourceConflict,
14503
14845
  sourceMaterializationIdentity,
14504
14846
  sourceStageRow,
14505
- sourceStageRows,
14506
- utf8SortKey as utf8SortKey2
14847
+ sourceStageRows
14507
14848
  } from "@sixb/core/internal/ontology-storage-provider";
14508
14849
 
14509
14850
  // src/ontology-storage/source-cleanup.ts
14851
+ function purgeAbandonedSourceVersions(db, input) {
14852
+ let remaining = input.limit;
14853
+ let rowsDeleted = 0;
14854
+ let materializationsDeleted = 0;
14855
+ const nextCandidate = db.query(`
14856
+ SELECT version_id FROM ontology_sources
14857
+ WHERE project_id = ? AND status = 'abandoned'
14858
+ ORDER BY terminal_at, source_id, materialization_id LIMIT 1
14859
+ `);
14860
+ const deleteRows = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
14861
+ SELECT rows.rowid FROM ontology_source_roots AS roots
14862
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
14863
+ WHERE roots.version_id = ? LIMIT ?)`);
14864
+ const deleteRoots = db.query(`DELETE FROM ontology_source_roots WHERE id IN (
14865
+ SELECT id FROM ontology_source_roots WHERE version_id = ? LIMIT ?)`);
14866
+ const deleteManifest = db.query("DELETE FROM ontology_sources WHERE version_id = ? AND status = 'abandoned'");
14867
+ while (remaining > 0) {
14868
+ const candidate = nextCandidate.get(input.projectId);
14869
+ if (!candidate)
14870
+ break;
14871
+ const rows = deleteRows.run(candidate.version_id, remaining).changes;
14872
+ rowsDeleted += rows;
14873
+ remaining -= rows;
14874
+ if (remaining === 0)
14875
+ break;
14876
+ const roots = deleteRoots.run(candidate.version_id, remaining).changes;
14877
+ rowsDeleted += roots;
14878
+ remaining -= roots;
14879
+ if (remaining === 0)
14880
+ break;
14881
+ const manifest = deleteManifest.run(candidate.version_id).changes;
14882
+ if (manifest === 0)
14883
+ break;
14884
+ materializationsDeleted += manifest;
14885
+ remaining -= manifest;
14886
+ }
14887
+ return { rowsDeleted, materializationsDeleted };
14888
+ }
14510
14889
  function cleanupSourceVersions(db, input) {
14511
14890
  let remaining = input.limit;
14891
+ const retired = `roots.project_id = ? AND roots.retired_at < ? AND versions.status = 'superseded'`;
14512
14892
  let rowsDeleted = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
14513
14893
  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
14894
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
14895
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
14896
+ WHERE ${retired}
14897
+ ORDER BY roots.retired_at, roots.id
14521
14898
  LIMIT ?
14522
14899
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14523
14900
  remaining -= rowsDeleted;
14524
14901
  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 ?
14902
+ const removed = db.query(`DELETE FROM ontology_source_roots WHERE id IN (
14903
+ SELECT roots.id FROM ontology_source_roots AS roots
14904
+ JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
14905
+ WHERE ${retired}
14906
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows WHERE rows.root_id = roots.id)
14907
+ ORDER BY roots.retired_at, roots.id LIMIT ?
14534
14908
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14535
14909
  rowsDeleted += removed;
14536
14910
  remaining -= removed;
14537
14911
  }
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 < ?
14912
+ const materializationsDeleted = remaining === 0 ? 0 : db.query(`DELETE FROM ontology_sources WHERE version_id IN (
14913
+ SELECT versions.version_id FROM ontology_sources AS versions
14914
+ WHERE project_id = ? AND status = 'superseded' AND terminal_at < ?
14541
14915
  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)
14916
+ WHERE roots.version_id = versions.version_id)
14547
14917
  ORDER BY terminal_at, source_id, materialization_id LIMIT ?
14548
14918
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14549
14919
  return { rowsDeleted, materializationsDeleted };
@@ -14575,7 +14945,7 @@ class SqliteOntologySourceStorage {
14575
14945
  LIMIT 1
14576
14946
  `).get(input.projectId, input.execution.projectionRunId);
14577
14947
  if (candidate) {
14578
- throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`);
14948
+ throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; adopt it before beginning another.`);
14579
14949
  }
14580
14950
  this.db.query(`
14581
14951
  INSERT INTO ontology_sources (
@@ -14601,10 +14971,10 @@ class SqliteOntologySourceStorage {
14601
14971
  throw sourceConflict(`Source materialization '${input.materializationId}' is '${manifest.status}' and cannot accept rows.`);
14602
14972
  }
14603
14973
  const rows = sourceStageRows(manifest.projection_kind, input.rows);
14604
- const existing = this.findStageRows(input, rows);
14974
+ const existing = this.findStageRows(manifest, rows);
14605
14975
  const { pending, unchanged } = reconcileSourceStageRows(rows, existing);
14606
14976
  stageSourceRoots(this.db, manifest, input);
14607
- this.insertStageRows(input, pending);
14977
+ this.insertStageRows(manifest, pending);
14608
14978
  return { inserted: pending.length, unchanged };
14609
14979
  });
14610
14980
  await yieldSqliteEventLoop();
@@ -14654,13 +15024,40 @@ class SqliteOntologySourceStorage {
14654
15024
  return row ? sourceRecord(row) : null;
14655
15025
  });
14656
15026
  }
15027
+ async adopt(input) {
15028
+ return this.runRootOperation(() => {
15029
+ assertProjectAndSource(input);
15030
+ assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
15031
+ assertTimestamp(input.adoptedAt, "Source adoptedAt", true);
15032
+ this.assertExecution(input);
15033
+ const candidate = this.findRunCandidate(input);
15034
+ if (!candidate)
15035
+ return null;
15036
+ const changed = this.db.query(`
15037
+ UPDATE ontology_sources SET execution_token = ?, updated_at = MAX(updated_at, ?)
15038
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?
15039
+ AND status IN ('staging', 'ready')
15040
+ `).run(input.execution.executionToken, input.adoptedAt, candidate.project_id, candidate.source_id, candidate.materialization_id).changes;
15041
+ if (changed !== 1) {
15042
+ throw sourceConflict(`Source materialization '${candidate.materialization_id}' changed.`);
15043
+ }
15044
+ const adopted = this.requireManifest(candidate.project_id, candidate.source_id, candidate.materialization_id);
15045
+ return {
15046
+ record: sourceRecord(adopted),
15047
+ resumeStagingOrdinal: adopted.status === "ready" ? Number(adopted.root_count) : this.lastStagedOrdinal(adopted)
15048
+ };
15049
+ });
15050
+ }
14657
15051
  async abandon(input) {
14658
15052
  return this.runRootOperation(() => {
14659
15053
  assertProjectAndSource(input);
14660
15054
  assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
14661
15055
  assertTimestamp(input.abandonedAt, "Source abandonedAt", true);
14662
15056
  this.assertExecution(input);
14663
- return input.kind === "candidate" ? this.abandonCandidate(input) : this.abandonForReclaim(input);
15057
+ if (input.kind === "candidate")
15058
+ return this.abandonCandidate(input);
15059
+ const candidate = this.findRunCandidate(input);
15060
+ return candidate ? this.transitionToAbandoned(candidate, input.abandonedAt) : null;
14664
15061
  });
14665
15062
  }
14666
15063
  async cleanupTerminal(input) {
@@ -14671,6 +15068,13 @@ class SqliteOntologySourceStorage {
14671
15068
  return cleanupSourceVersions(this.db, input);
14672
15069
  });
14673
15070
  }
15071
+ async purgeAbandoned(input) {
15072
+ return this.runRootOperation(() => {
15073
+ assertNonblank(input.projectId, "Abandoned source purge project id");
15074
+ assertPositiveInteger(input.limit, "Abandoned source purge limit");
15075
+ return purgeAbandonedSourceVersions(this.db, input);
15076
+ });
15077
+ }
14674
15078
  async summarizeTerminal(input) {
14675
15079
  return this.runRootOperation(() => {
14676
15080
  assertNonblank(input.projectId, "Terminal source summary project id");
@@ -14696,19 +15100,20 @@ class SqliteOntologySourceStorage {
14696
15100
  assertSourceCandidateOwner(sourceRecord(manifest), input.execution);
14697
15101
  return this.transitionToAbandoned(manifest, input.abandonedAt);
14698
15102
  }
14699
- abandonForReclaim(input) {
15103
+ findRunCandidate(input) {
14700
15104
  const candidates = this.db.query(`
14701
15105
  SELECT * FROM ontology_sources
14702
15106
  WHERE project_id = ? AND source_id = ? AND projection_run_id = ?
14703
15107
  AND status IN ('staging', 'ready')
14704
15108
  `).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
15109
  if (candidates.length > 1) {
14709
15110
  throw sourceConflict(`Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`);
14710
15111
  }
14711
- return candidates[0] ? this.transitionToAbandoned(candidates[0], input.abandonedAt) : null;
15112
+ return candidates[0] ?? null;
15113
+ }
15114
+ lastStagedOrdinal(manifest) {
15115
+ const last = this.db.query("SELECT MAX(staging_ordinal) AS ordinal FROM ontology_source_roots WHERE version_id = ?").get(manifest.version_id);
15116
+ return last?.ordinal ?? 0;
14712
15117
  }
14713
15118
  transitionToAbandoned(manifest, abandonedAt) {
14714
15119
  if (abandonedAt < manifest.created_at || manifest.ready_at !== null && abandonedAt < manifest.ready_at) {
@@ -14723,84 +15128,95 @@ class SqliteOntologySourceStorage {
14723
15128
  if (changed !== 1) {
14724
15129
  throw sourceConflict(`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`);
14725
15130
  }
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
15131
  return sourceRecord(this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id));
14729
15132
  }
14730
15133
  assertReadyState(manifest, rootCount, assertionCount) {
14731
15134
  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,
15135
+ SELECT (SELECT COUNT(*) FROM ontology_source_roots AS roots
15136
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15137
+ WHERE roots.version_id = ?1) AS assertions,
15138
+ COUNT(*) AS roots, COUNT(DISTINCT staging_ordinal) AS ordinals,
14735
15139
  MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
14736
15140
  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);
15141
+ WHERE version_id = ?1
15142
+ `).get(manifest.version_id);
14739
15143
  if (counts.assertions !== assertionCount || counts.roots !== rootCount || counts.ordinals !== rootCount || rootCount > 0 && (counts.min_ordinal !== 0 || counts.max_ordinal !== rootCount - 1)) {
14740
15144
  throw new MaterializationValidationError4("Source ready counts do not match the staged roots and assertions.");
14741
15145
  }
14742
15146
  assertSourceRootCoverage(this.db, manifest);
14743
15147
  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)
15148
+ SELECT roots.root_key
15149
+ FROM ontology_source_roots AS roots
15150
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15151
+ WHERE roots.version_id = ?
15152
+ GROUP BY roots.id
15153
+ HAVING COUNT(*) <> 1
15154
+ OR MAX(json_extract(roots.root_key, '$[0]') <> 'link')
15155
+ OR MAX(rows.entity_kind <> 'link')
15156
+ OR MAX(rows.source_type_id <> json_extract(roots.root_key, '$[1]')
15157
+ OR rows.source_primary_id <> json_extract(roots.root_key, '$[2]')
15158
+ OR rows.link_id <> json_extract(roots.root_key, '$[3]')
15159
+ OR rows.target_type_id <> json_extract(roots.root_key, '$[4]')
15160
+ OR rows.target_primary_id <> json_extract(roots.root_key, '$[5]'))
14750
15161
  LIMIT 1
14751
15162
  ` : `
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
15163
+ SELECT roots.root_key
15164
+ FROM ontology_source_roots AS roots
15165
+ JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15166
+ WHERE roots.version_id = ?
15167
+ GROUP BY roots.id
15168
+ HAVING MAX(json_extract(roots.root_key, '$[0]') <> 'object')
15169
+ OR SUM(rows.entity_kind = 'object'
15170
+ AND rows.object_type_id = json_extract(roots.root_key, '$[1]')
15171
+ AND rows.primary_id = json_extract(roots.root_key, '$[2]')) <> 1
15172
+ OR SUM(
15173
+ (rows.entity_kind = 'object' AND (
15174
+ rows.object_type_id <> json_extract(roots.root_key, '$[1]')
15175
+ OR rows.primary_id <> json_extract(roots.root_key, '$[2]')))
15176
+ OR (rows.entity_kind = 'link' AND (
15177
+ rows.source_type_id <> json_extract(roots.root_key, '$[1]')
15178
+ OR rows.source_primary_id <> json_extract(roots.root_key, '$[2]')))
15179
+ ) <> 0
15180
+ OR SUM(rows.entity_kind = 'link') <> COUNT(DISTINCT CASE
15181
+ WHEN rows.entity_kind = 'link'
15182
+ THEN json_array(rows.link_id, rows.target_type_id, rows.target_primary_id)
15183
+ END)
14765
15184
  LIMIT 1
14766
- `).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
15185
+ `).get(manifest.version_id);
14767
15186
  if (invalid) {
14768
15187
  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
15188
  }
14770
15189
  }
14771
- findStageRows(input, rows) {
15190
+ findStageRows(manifest, rows) {
14772
15191
  if (rows.length === 0)
14773
15192
  return [];
14774
- const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.rootKey)))];
15193
+ const rootKeys = [...new Set(rows.map((row) => row.rootKey))];
14775
15194
  const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))];
14776
- const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.entityKey)))];
14777
15195
  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));
15196
+ WITH selected AS (
15197
+ SELECT roots.id FROM json_each(?2) AS requested
15198
+ CROSS JOIN ontology_source_roots AS roots
15199
+ ON roots.version_id = ?1 AND roots.root_key = CAST(requested.value AS TEXT)
15200
+ UNION
15201
+ SELECT roots.id FROM json_each(?3) AS requested
15202
+ CROSS JOIN ontology_source_roots AS roots
15203
+ ON roots.version_id = ?1 AND roots.staging_ordinal = CAST(requested.value AS INTEGER)
15204
+ )
15205
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM selected
15206
+ CROSS JOIN ontology_source_roots AS roots ON roots.id = selected.id
15207
+ CROSS JOIN ontology_sources AS versions ON versions.version_id = roots.version_id
15208
+ CROSS JOIN ontology_source_rows AS rows ON rows.root_id = roots.id
15209
+ `).all(manifest.version_id, JSON.stringify(rootKeys), JSON.stringify(ordinals));
14786
15210
  return existing.map((row) => sourceStageRow(sourceAssertion(row)));
14787
15211
  }
14788
- insertStageRows(input, rows) {
15212
+ insertStageRows(manifest, rows) {
14789
15213
  if (rows.length === 0)
14790
15214
  return;
14791
- const payload = rows.map(({ entityKey, rootKey, row }) => {
15215
+ const payload = rows.map(({ rootKey, row }) => {
14792
15216
  const entity = entityColumns(row.assertion);
14793
- const root = entityColumns(row.root);
14794
15217
  return {
14795
- entityKind: row.assertion.kind,
14796
- entityKey,
14797
- entitySortKey: utf8SortKey2(entityKey),
14798
- rootKind: row.root.kind,
14799
15218
  rootKey,
14800
- rootSortKey: utf8SortKey2(rootKey),
14801
- stagingOrdinal: row.stagingOrdinal,
14802
- root: row.root,
14803
- assertion: row.assertion,
15219
+ entityKind: row.assertion.kind,
14804
15220
  objectTypeId: entity.objectTypeId,
14805
15221
  primaryId: entity.primaryId,
14806
15222
  sourceTypeId: entity.sourceTypeId,
@@ -14808,45 +15224,27 @@ class SqliteOntologySourceStorage {
14808
15224
  linkId: entity.linkId,
14809
15225
  targetTypeId: entity.targetTypeId,
14810
15226
  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
15227
+ payload: sourceAssertionPayload(row.assertion)
14818
15228
  };
14819
15229
  });
14820
- this.db.query(`
15230
+ const inserted = this.db.query(`
14821
15231
  WITH staged(value) AS (SELECT value FROM json_each(?))
14822
15232
  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
15233
+ root_id, entity_kind, object_type_id, primary_id,
15234
+ source_type_id, source_primary_id, link_id, target_type_id, target_primary_id, payload
14832
15235
  )
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')),
15236
+ SELECT roots.id, json_extract(value, '$.entityKind'),
14840
15237
  json_extract(value, '$.objectTypeId'), json_extract(value, '$.primaryId'),
14841
15238
  json_extract(value, '$.sourceTypeId'), json_extract(value, '$.sourcePrimaryId'),
14842
15239
  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')
15240
+ json_extract(value, '$.targetPrimaryId'), json_extract(value, '$.payload')
14848
15241
  FROM staged
14849
- `).run(canonicalJson(payload), input.projectId, input.source.projectionId, input.materializationId);
15242
+ CROSS JOIN ontology_source_roots AS roots
15243
+ ON roots.version_id = ? AND roots.root_key = json_extract(value, '$.rootKey')
15244
+ `).run(canonicalJson(payload), manifest.version_id).changes;
15245
+ if (inserted !== rows.length) {
15246
+ throw sourceConflict("Staged source rows do not all belong to a staged root.");
15247
+ }
14850
15248
  }
14851
15249
  assertExecution(input, identity) {
14852
15250
  assertProjectionExecution(this.db, {
@@ -15113,6 +15511,8 @@ function queryLatestRunsByOwnerId(input) {
15113
15511
  return [];
15114
15512
  }
15115
15513
  const placeholders = ownerIds.map(() => "?").join(", ");
15514
+ const statusFilter = input.excludedStatus === undefined ? "" : " AND status <> ?";
15515
+ const statusArgs = input.excludedStatus === undefined ? [] : [input.excludedStatus];
15116
15516
  const orderColumn = input.tableName === "sync_runs" || input.tableName === "pipeline_runs" || input.tableName === "projection_runs" ? "COALESCE(started_at, queued_at)" : "started_at";
15117
15517
  const rows = input.db.query(`
15118
15518
  SELECT *
@@ -15124,10 +15524,10 @@ function queryLatestRunsByOwnerId(input) {
15124
15524
  ORDER BY ${orderColumn} DESC, id DESC
15125
15525
  ) AS sixb_latest_rank
15126
15526
  FROM ${input.tableName}
15127
- WHERE project_id = ? AND ${input.ownerColumn} IN (${placeholders})
15527
+ WHERE project_id = ? AND ${input.ownerColumn} IN (${placeholders})${statusFilter}
15128
15528
  )
15129
15529
  WHERE sixb_latest_rank = 1
15130
- `).all(input.projectId, ...ownerIds);
15530
+ `).all(input.projectId, ...ownerIds, ...statusArgs);
15131
15531
  const latestByOwnerId = new Map(rows.map((row) => [input.ownerIdFor(row), row]));
15132
15532
  return ownerIds.flatMap((ownerId) => {
15133
15533
  const row = latestByOwnerId.get(ownerId);
@@ -15473,12 +15873,14 @@ import {
15473
15873
  failProjectionRunEnqueue,
15474
15874
  immutableDatasetVersionConflict,
15475
15875
  mergeProjectionRunProgress,
15876
+ parseProjectionAttemptFailure,
15476
15877
  planProjectionRunFinish,
15477
15878
  planProjectionRunReclaim,
15478
15879
  projectionRunNotFound,
15479
15880
  publicProjectionRunRecord,
15480
15881
  requireTelemetryProjectionRun,
15481
15882
  restoreProjectionRun,
15883
+ SUPERSEDING_PROJECTION_RUN_STATUSES,
15482
15884
  staleProjectionRunExecution
15483
15885
  } from "@sixb/core/internal/projection-run-storage-provider";
15484
15886
  import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage";
@@ -15560,7 +15962,7 @@ class SqliteProjectionRunStorage {
15560
15962
  const result = this.db.query(`
15561
15963
  UPDATE projection_runs
15562
15964
  SET status = 'running', started_at = COALESCE(started_at, ?), finished_at = NULL,
15563
- attempt = ?, execution_token = ?, error = NULL
15965
+ attempt = ?, execution_token = ?
15564
15966
  WHERE project_id = ? AND id = ?
15565
15967
  AND (
15566
15968
  (status = 'queued' AND execution_token IS NULL)
@@ -15604,6 +16006,20 @@ class SqliteProjectionRunStorage {
15604
16006
  return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id));
15605
16007
  });
15606
16008
  }
16009
+ async recordAttemptFailure(input) {
16010
+ return runImmediateTransaction(this.db, () => {
16011
+ const existingRow = this.requireMaterializationExecution(input);
16012
+ const error = parseProjectionAttemptFailure(restoreProjectionRunRow(existingRow), input.error);
16013
+ const result = this.db.query(`
16014
+ UPDATE projection_runs
16015
+ SET error = ?
16016
+ WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
16017
+ `).run(serializeSixbFailure4(error, PROJECTION_RUN_FAILURE_CODES), input.projectId, input.id, input.executionToken);
16018
+ if (result.changes !== 1)
16019
+ throw staleProjectionRunExecution(input.id);
16020
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id));
16021
+ });
16022
+ }
15607
16023
  async finish(input) {
15608
16024
  return runImmediateTransaction(this.db, () => {
15609
16025
  const existingRow = this.requireMaterializationExecution(input);
@@ -15741,6 +16157,21 @@ class SqliteProjectionRunStorage {
15741
16157
  const runs = rows.map(rowToProjectionRunRecord);
15742
16158
  return { runs, hasMore: offset + runs.length < totalRow.count, total: totalRow.count };
15743
16159
  }
16160
+ async findSupersedingRun(input) {
16161
+ const run = this.requireRow(input.projectId, input.id);
16162
+ if (run.materialization_protocol !== "replacement")
16163
+ return null;
16164
+ const statuses = SUPERSEDING_PROJECTION_RUN_STATUSES;
16165
+ const newer = this.db.query(`
16166
+ SELECT * FROM projection_runs
16167
+ WHERE project_id = ? AND projection_id = ? AND materialization_protocol = 'replacement'
16168
+ AND status IN (${statuses.map(() => "?").join(", ")})
16169
+ AND dataset_version_created_at > ?
16170
+ ORDER BY dataset_version_created_at DESC, id DESC
16171
+ LIMIT 1
16172
+ `).get(input.projectId, run.projection_id, ...statuses, run.dataset_version_created_at);
16173
+ return newer ? rowToProjectionRunRecord(newer) : null;
16174
+ }
15744
16175
  async listLatestByProjectionIds(input) {
15745
16176
  const rows = queryLatestRunsByOwnerId({
15746
16177
  db: this.db,
@@ -15748,7 +16179,8 @@ class SqliteProjectionRunStorage {
15748
16179
  ownerColumn: "projection_id",
15749
16180
  ownerIds: input.projectionIds,
15750
16181
  projectId: input.projectId,
15751
- ownerIdFor: (row) => row.projection_id
16182
+ ownerIdFor: (row) => row.projection_id,
16183
+ excludedStatus: "superseded"
15752
16184
  });
15753
16185
  return { runs: rows.map(rowToProjectionRunRecord) };
15754
16186
  }
@@ -17945,4 +18377,4 @@ export {
17945
18377
  migrateSqliteStorage
17946
18378
  };
17947
18379
 
17948
- //# debugId=9F25188860B63C3464756E2164756E21
18380
+ //# debugId=1D3AEE45CA656B6464756E2164756E21