@sixb/sqlite 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/auth-storage/auth-storage.d.ts +2 -1
  2. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  3. package/dist/auth-storage/rows.d.ts +1 -1
  4. package/dist/auth-storage/rows.d.ts.map +1 -1
  5. package/dist/index.js +726 -286
  6. package/dist/index.js.map +12 -12
  7. package/dist/latest-run-query.d.ts +2 -0
  8. package/dist/latest-run-query.d.ts.map +1 -1
  9. package/dist/migrations.d.ts.map +1 -1
  10. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  11. package/dist/ontology-storage/shared.d.ts +11 -8
  12. package/dist/ontology-storage/shared.d.ts.map +1 -1
  13. package/dist/ontology-storage/source-cleanup.d.ts +7 -1
  14. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -1
  15. package/dist/ontology-storage/source-roots.d.ts +14 -2
  16. package/dist/ontology-storage/source-roots.d.ts.map +1 -1
  17. package/dist/ontology-storage/sources.d.ts +8 -3
  18. package/dist/ontology-storage/sources.d.ts.map +1 -1
  19. package/dist/projection-run-storage.d.ts +3 -1
  20. package/dist/projection-run-storage.d.ts.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth-storage/auth-storage.ts +44 -79
  23. package/src/auth-storage/rows.ts +1 -1
  24. package/src/latest-run-query.ts +8 -2
  25. package/src/migrations/050-directory-group-memberships.sql +21 -0
  26. package/src/migrations/051-projection-run-supersession.sql +126 -0
  27. package/src/migrations/052-compact-source-storage.sql +181 -0
  28. package/src/migrations.ts +42 -0
  29. package/src/ontology-storage/materialization-state.ts +34 -44
  30. package/src/ontology-storage/shared.ts +20 -11
  31. package/src/ontology-storage/source-cleanup.ts +61 -24
  32. package/src/ontology-storage/source-roots.ts +166 -110
  33. package/src/ontology-storage/sources.ts +169 -118
  34. package/src/projection-run-storage.ts +59 -1
package/dist/index.js CHANGED
@@ -4333,6 +4333,343 @@ ALTER TABLE auth_device_authorizations DROP COLUMN token_name;
4333
4333
  ALTER TABLE auth_device_authorizations DROP COLUMN token_expires_at;
4334
4334
  `;
4335
4335
 
4336
+ // src/migrations/050-directory-group-memberships.sql
4337
+ var _050_directory_group_memberships_default = `-- A membership the identity provider owns is recorded with source 'directory' and synced to the
4338
+ -- provider's groups on every sign-in. Service accounts never sign in, so their table is unchanged.
4339
+ -- SQLite cannot change a CHECK constraint in place, so the table is rebuilt.
4340
+ CREATE TABLE auth_group_memberships_next (
4341
+ project_id TEXT NOT NULL,
4342
+ user_id TEXT NOT NULL,
4343
+ group_id TEXT NOT NULL,
4344
+ source TEXT NOT NULL CHECK (source IN ('invitation', 'manual', 'agent', 'directory')),
4345
+ created_at TEXT NOT NULL,
4346
+ PRIMARY KEY (project_id, user_id, group_id)
4347
+ );
4348
+
4349
+ INSERT INTO auth_group_memberships_next (project_id, user_id, group_id, source, created_at)
4350
+ SELECT project_id, user_id, group_id, source, created_at
4351
+ FROM auth_group_memberships;
4352
+
4353
+ DROP TABLE auth_group_memberships;
4354
+ ALTER TABLE auth_group_memberships_next RENAME TO auth_group_memberships;
4355
+
4356
+ CREATE INDEX idx_auth_group_memberships_group
4357
+ ON auth_group_memberships(project_id, group_id);
4358
+ `;
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
+
4336
4673
  // src/migrations/037-execution-requester-groups.sql
4337
4674
  var _037_execution_requester_groups_default = `ALTER TABLE executions ADD COLUMN requester_group_ids TEXT NOT NULL DEFAULT '[]';
4338
4675
 
@@ -4536,7 +4873,17 @@ var sqliteStorageMigrations = defineMigrations({
4536
4873
  sqliteSql("046-workflow-intervention-principals", _046_workflow_intervention_principals_default),
4537
4874
  sqliteSql("047-ontology-commit-attribution", _047_ontology_commit_attribution_default),
4538
4875
  sqliteSql("048-file-upload-sessions", _048_file_upload_sessions_default),
4539
- sqliteSql("049-native-sessions", _049_native_sessions_default)
4876
+ sqliteSql("049-native-sessions", _049_native_sessions_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) })
4540
4887
  ]
4541
4888
  });
4542
4889
  function sqliteStoragePath(basePath) {
@@ -4651,6 +4998,11 @@ function rowToMigrationRecord(row) {
4651
4998
  finishedAt: migration.finished_at ?? undefined
4652
4999
  };
4653
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
+ }
4654
5006
  function checksum(value) {
4655
5007
  return createHash("sha256").update(value).digest("hex");
4656
5008
  }
@@ -9135,17 +9487,6 @@ class SqliteAuthStorage {
9135
9487
  error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
9136
9488
  };
9137
9489
  }
9138
- if (shouldCreateUser && !activeInvitation && input.allowUserCreationWithoutInvitation && input.requireNoActiveUsersForUserCreation && hasActiveUsers(this.db, projectId)) {
9139
- consumeMagicLink(this.db, {
9140
- projectId,
9141
- id: input.magicLinkId,
9142
- tokenHash: input.tokenHash,
9143
- consumedAt: completedAt
9144
- });
9145
- return {
9146
- error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
9147
- };
9148
- }
9149
9490
  validateCompleteSessionInput(this.db, projectId, input.session);
9150
9491
  assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience);
9151
9492
  let newUserId;
@@ -9175,8 +9516,9 @@ class SqliteAuthStorage {
9175
9516
  projectId,
9176
9517
  user
9177
9518
  });
9178
- const groupMemberships = this.applyManualGroups({
9519
+ const groupMemberships = this.applyGroups({
9179
9520
  completedAt,
9521
+ source: "manual",
9180
9522
  existing: invitation.groupMemberships,
9181
9523
  groupIds: manualGroupIds,
9182
9524
  projectId,
@@ -9230,36 +9572,18 @@ class SqliteAuthStorage {
9230
9572
  error: new AuthStorageError11("missing_user", `[Sixb] User '${identity.user_id}' not found for linked OIDC identity.`)
9231
9573
  };
9232
9574
  }
9233
- if (!identity && userRow && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
9234
- this.consumeOidcAttempt(input, completedAt, projectId);
9235
- return {
9236
- error: new AuthStorageError11("email_link_not_allowed", `[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`)
9237
- };
9238
- }
9239
9575
  if (userRow?.status === "suspended") {
9240
9576
  this.consumeOidcAttempt(input, completedAt, projectId);
9241
9577
  return {
9242
9578
  error: new AuthStorageError11("suspended_user", `[Sixb] User '${userRow.id}' is suspended for project '${projectId}'.`)
9243
9579
  };
9244
9580
  }
9245
- if (shouldCreateUser && !input.emailVerified) {
9246
- this.consumeOidcAttempt(input, completedAt, projectId);
9247
- return {
9248
- error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
9249
- };
9250
- }
9251
9581
  if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
9252
9582
  this.consumeOidcAttempt(input, completedAt, projectId);
9253
9583
  return {
9254
9584
  error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
9255
9585
  };
9256
9586
  }
9257
- if (shouldCreateUser && !activeInvitation && input.allowUserCreationWithoutInvitation && input.requireNoActiveUsersForUserCreation && hasActiveUsers(this.db, projectId)) {
9258
- this.consumeOidcAttempt(input, completedAt, projectId);
9259
- return {
9260
- error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
9261
- };
9262
- }
9263
9587
  validateCompleteSessionInput(this.db, projectId, input.session);
9264
9588
  assertSignInSessionAudience(projectId, input.session.audience, attempt.audience);
9265
9589
  let newUserId;
@@ -9297,10 +9621,17 @@ class SqliteAuthStorage {
9297
9621
  projectId,
9298
9622
  user
9299
9623
  });
9300
- const groupMemberships = this.applyManualGroups({
9624
+ const groupMemberships = this.syncDirectoryGroups({
9301
9625
  completedAt,
9302
- existing: invitation.groupMemberships,
9303
- groupIds: manualGroupIds,
9626
+ existing: this.applyGroups({
9627
+ completedAt,
9628
+ source: "manual",
9629
+ existing: invitation.groupMemberships,
9630
+ groupIds: manualGroupIds,
9631
+ projectId,
9632
+ userId: user.id
9633
+ }),
9634
+ groupIds: input.directoryGroupIds,
9304
9635
  projectId,
9305
9636
  userId: user.id
9306
9637
  });
@@ -9440,19 +9771,33 @@ class SqliteAuthStorage {
9440
9771
  }));
9441
9772
  return { invitation, groupMemberships };
9442
9773
  }
9443
- applyManualGroups(input) {
9774
+ applyGroups(input) {
9444
9775
  const groupMemberships = [...input.existing];
9445
9776
  for (const groupId of input.groupIds) {
9446
9777
  groupMemberships.push(upsertGroupMembership(this.db, {
9447
9778
  projectId: input.projectId,
9448
9779
  userId: input.userId,
9449
9780
  groupId,
9450
- source: "manual",
9781
+ source: input.source,
9451
9782
  createdAt: input.completedAt
9452
9783
  }));
9453
9784
  }
9454
9785
  return groupMemberships;
9455
9786
  }
9787
+ syncDirectoryGroups(input) {
9788
+ if (!input.groupIds) {
9789
+ return input.existing;
9790
+ }
9791
+ const groupIds = normalizeGroupIds(input.groupIds);
9792
+ this.db.query(`
9793
+ DELETE FROM auth_group_memberships
9794
+ WHERE project_id = ?
9795
+ AND user_id = ?
9796
+ AND source = 'directory'
9797
+ AND group_id NOT IN (${groupIds.map(() => "?").join(", ")})
9798
+ `).run(input.projectId, input.userId, ...groupIds);
9799
+ return this.applyGroups({ ...input, groupIds, source: "directory" });
9800
+ }
9456
9801
  createSignInSession(input) {
9457
9802
  return createSession(this.db, {
9458
9803
  id: input.session.id,
@@ -9504,16 +9849,6 @@ class SqliteAuthStorage {
9504
9849
  });
9505
9850
  }
9506
9851
  }
9507
- function hasActiveUsers(db, projectId) {
9508
- const row = db.query(`
9509
- SELECT 1 AS active
9510
- FROM auth_users
9511
- WHERE project_id = ?
9512
- AND status = 'active'
9513
- LIMIT 1
9514
- `).get(projectId);
9515
- return row !== null;
9516
- }
9517
9852
  function assertSignInSessionAudience(projectId, sessionAudience, storedAudience) {
9518
9853
  if (sessionAudience === storedAudience) {
9519
9854
  return;
@@ -12196,6 +12531,10 @@ function assertReconciliationPageLimit(limit) {
12196
12531
  // src/ontology-storage/shared.ts
12197
12532
  import { parseSixbFailure as parseSixbFailure2 } from "@sixb/core/internal/errors";
12198
12533
  import { MaterializationConflictError } from "@sixb/core/internal/materialization";
12534
+ import {
12535
+ sourceAssertionFromColumns,
12536
+ sourceEntityFromKey
12537
+ } from "@sixb/core/internal/ontology-storage-provider";
12199
12538
  import { ONTOLOGY_OUTBOX_FAILURE_CODES } from "@sixb/core/storage";
12200
12539
  import {
12201
12540
  assertNonblank,
@@ -12248,8 +12587,8 @@ function sourceRecord(row) {
12248
12587
  }
12249
12588
  function sourceAssertion(row) {
12250
12589
  return {
12251
- root: parseJson2(row.root),
12252
- assertion: parseJson2(row.assertion),
12590
+ root: sourceEntityFromKey(row.root_key),
12591
+ assertion: sourceAssertionFromColumns(row, row.payload === null ? null : parseJson2(row.payload)),
12253
12592
  stagingOrdinal: row.staging_ordinal
12254
12593
  };
12255
12594
  }
@@ -12467,29 +12806,68 @@ import {
12467
12806
  MaterializationValidationError,
12468
12807
  projectionEntityKey
12469
12808
  } from "@sixb/core/internal/materialization";
12470
- import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider";
12471
- function replacementSourceRows(input) {
12472
- const roots = input.incremental ? `ontology_source_roots AS changed
12473
- CROSS JOIN ontology_source_roots AS roots ON roots.project_id = changed.project_id
12474
- 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";
12475
- 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);
12476
12841
  return {
12477
- 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 = ?
12478
12867
  UNION ALL
12479
- SELECT rows.* FROM ${roots}
12480
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id
12481
- AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
12482
- AND rows.root_sort_key = roots.root_sort_key
12483
- WHERE ${selected}.project_id = ? AND ${selected}.source_id = ?
12484
- AND ${input.incremental ? "changed.materialization_id = ?" : "roots.active = 1"}`,
12485
- values: [
12486
- input.projectId,
12487
- input.sourceId,
12488
- input.materializationId,
12489
- input.projectId,
12490
- input.sourceId,
12491
- ...input.incremental ? [input.materializationId] : []
12492
- ]
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]
12493
12871
  };
12494
12872
  }
12495
12873
  function stageSourceRoots(db, manifest, input) {
@@ -12498,15 +12876,15 @@ function stageSourceRoots(db, manifest, input) {
12498
12876
  throw new MaterializationValidationError("Root deletions require a source delta base.");
12499
12877
  }
12500
12878
  const existing = db.query(`SELECT staging_ordinal, deleted FROM ontology_source_roots
12501
- WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND root_sort_key = ?`);
12879
+ WHERE version_id = ? AND root_key = ?`);
12502
12880
  const insert = db.query(`INSERT INTO ontology_source_roots (
12503
- project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
12504
- ) 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`);
12505
12883
  for (const root of roots) {
12506
- 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));
12507
12885
  if (inserted)
12508
12886
  continue;
12509
- const previous = existing.get(input.projectId, input.source.projectionId, input.materializationId, root.sortKey);
12887
+ const previous = existing.get(manifest.version_id, root.rootKey);
12510
12888
  if (!previous || previous.staging_ordinal !== root.stagingOrdinal || Boolean(previous.deleted) !== root.deleted) {
12511
12889
  throw new MaterializationValidationError("Source materialization repeats root or stream ordinal with different content.");
12512
12890
  }
@@ -12517,72 +12895,61 @@ function activateSourceRoots(db, projectId, candidate, activation) {
12517
12895
  throw new MaterializationConflictError2("projection-fence", "Source delta base changed before activation.");
12518
12896
  if (candidate.base_materialization_id !== null && candidate.root_count === 0)
12519
12897
  return;
12520
- const scope = [projectId, activation.source.projectionId, activation.materializationId];
12521
- db.query(`UPDATE ontology_source_roots AS roots SET active = 0, retired_at = ?
12522
- WHERE project_id = ? AND source_id = ? AND active = 1
12523
- ${candidate.base_materialization_id === null ? "" : `AND root_sort_key IN (
12524
- SELECT root_sort_key FROM ontology_source_roots
12525
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?)`}`).run(activation.updatedAt, projectId, activation.source.projectionId, ...candidate.base_materialization_id === null ? [] : [...scope]);
12526
- db.query(`UPDATE ontology_source_roots SET active = CASE WHEN deleted = 1 THEN 0 ELSE 1 END,
12527
- retired_at = CASE WHEN deleted = 1 THEN ? ELSE NULL END
12528
- 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);
12529
12907
  }
12530
12908
  function assertSourceRootCoverage(db, manifest) {
12531
12909
  const invalid = db.query(`SELECT 1 FROM ontology_source_roots AS roots
12532
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
12910
+ WHERE roots.version_id = ?
12533
12911
  AND roots.deleted = EXISTS (
12534
- SELECT 1 FROM ontology_source_rows AS rows
12535
- WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12536
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
12537
- ) 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);
12538
12914
  if (invalid)
12539
12915
  throw new MaterializationValidationError("Source roots must have complete assertions or an explicit deletion, never both.");
12540
12916
  }
12541
12917
  function activeSourceRows(db, projectId, refs) {
12542
- const keys = refs.flatMap((entity) => {
12543
- const entitySortKey = utf8SortKey(projectionEntityKey(entity));
12544
- const roots = entity.kind === "object" ? [entitySortKey] : [
12545
- entitySortKey,
12546
- utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source }))
12547
- ];
12548
- 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 }));
12549
12921
  });
12550
12922
  return db.query(`WITH requested AS (
12551
- SELECT DISTINCT json_extract(value, '$.rootSortKey') AS root_sort_key,
12552
- json_extract(value, '$.entitySortKey') AS entity_sort_key FROM json_each(?)
12553
- ) 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
12554
12926
  CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
12555
- AND roots.active = 1 AND roots.root_sort_key = requested.root_sort_key
12556
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12557
- AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
12558
- 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);
12559
12931
  }
12560
12932
  function replacementAssertionRows(db, input) {
12561
- const keys = input.refs.map((ref) => ({
12562
- entityKind: ref.kind,
12563
- entityKey: projectionEntityKey(ref),
12564
- rootKeys: ref.kind === "object" ? [utf8SortKey(projectionEntityKey(ref))] : [
12565
- utf8SortKey(projectionEntityKey(ref)),
12566
- utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source }))
12567
- ]
12568
- }));
12933
+ const version = sourceVersionId(db, input);
12569
12934
  const previous = !input.includePrevious ? "" : `UNION ALL
12570
- SELECT rows.* FROM requested_entities AS requested
12571
- CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ? AND roots.source_id = ?
12572
- AND roots.active = 1 AND roots.root_sort_key IN (SELECT value FROM json_each(requested.root_keys))
12573
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12574
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
12575
- 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}
12576
12942
  ${!input.incremental ? "" : `WHERE EXISTS (SELECT 1 FROM ontology_source_roots AS changed
12577
- WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
12578
- 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)`}`;
12579
12944
  return db.query(`WITH requested_entities AS (
12580
- SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
12581
- json_extract(value, '$.entityKey') AS entity_key, json_extract(value, '$.rootKeys') AS root_keys FROM json_each(?)
12582
- ) SELECT rows.* FROM requested_entities AS requested
12583
- CROSS JOIN ontology_source_rows AS rows ON rows.project_id = ? AND rows.source_id = ?
12584
- AND rows.materialization_id = ? AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
12585
- ${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] : []] : []);
12586
12953
  }
12587
12954
 
12588
12955
  // src/ontology-storage/materialization-state.ts
@@ -12795,19 +13162,13 @@ class SqliteMaterializationStateReader {
12795
13162
  json_extract(value, '$.linkId') AS link_id
12796
13163
  FROM json_each(?)
12797
13164
  )
12798
- SELECT rows.* FROM requested
13165
+ SELECT ${SOURCE_ASSERTION_COLUMNS} FROM requested
12799
13166
  CROSS JOIN ontology_source_rows AS rows
12800
- ON rows.project_id = ?
12801
- AND rows.entity_kind = 'link'
13167
+ ON rows.entity_kind = 'link'
12802
13168
  AND rows.source_type_id = requested.source_type_id
12803
13169
  AND rows.source_primary_id = requested.source_primary_id
12804
13170
  AND rows.link_id = requested.link_id
12805
- JOIN ontology_source_roots AS sources
12806
- ON sources.project_id = rows.project_id
12807
- AND sources.source_id = rows.source_id
12808
- AND sources.materialization_id = rows.materialization_id
12809
- AND sources.root_sort_key = rows.root_sort_key
12810
- WHERE sources.active = 1`).all(requestedJson, this.projectId);
13171
+ ${LIVE_SOURCE_ROWS_JOIN}`).all(requestedJson, this.projectId);
12811
13172
  const slotOverrideRows = this.db.query(`WITH requested AS (
12812
13173
  SELECT DISTINCT
12813
13174
  json_extract(value, '$.sourceTypeId') AS source_type_id,
@@ -12896,12 +13257,8 @@ class SqliteMaterializationStateReader {
12896
13257
  SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
12897
13258
  rows.target_type_id, rows.target_primary_id
12898
13259
  FROM ontology_source_rows AS rows
12899
- JOIN ontology_source_roots AS sources
12900
- ON sources.project_id = rows.project_id
12901
- AND sources.source_id = rows.source_id
12902
- AND sources.materialization_id = rows.materialization_id
12903
- AND sources.root_sort_key = rows.root_sort_key
12904
- WHERE rows.project_id = ? AND rows.entity_kind = 'link' AND sources.active = 1
13260
+ ${LIVE_SOURCE_ROWS_JOIN}
13261
+ WHERE rows.entity_kind = 'link'
12905
13262
  ), selected AS (
12906
13263
  SELECT DISTINCT links.*,
12907
13264
  ${linkSortExpression("links")} AS sort_key
@@ -13012,7 +13369,7 @@ class SqliteMaterializationStateReader {
13012
13369
  this.prepareReplacementLinks(input);
13013
13370
  }
13014
13371
  prepareReplacementObjects(input) {
13015
- const rows = replacementSourceRows({
13372
+ const rows = replacementSourceRows(this.db, {
13016
13373
  projectId: this.projectId,
13017
13374
  sourceId: input.sourceId,
13018
13375
  materializationId: input.candidateMaterializationId,
@@ -13023,14 +13380,14 @@ class SqliteMaterializationStateReader {
13023
13380
  session_id, entity_kind, identity_key, sort_key, diff_required
13024
13381
  )
13025
13382
  SELECT ?, 'object', json_array(object_type_id, primary_id),
13026
- MIN(entity_sort_key), 1
13383
+ ${objectSortExpression()}, 1
13027
13384
  FROM (${rows.sql}) AS rows
13028
13385
  WHERE entity_kind = 'object'
13029
13386
  GROUP BY object_type_id, primary_id
13030
13387
  `).run(input.sessionId, ...rows.values);
13031
13388
  }
13032
13389
  prepareReplacementLinks(input) {
13033
- const rows = replacementSourceRows({
13390
+ const rows = replacementSourceRows(this.db, {
13034
13391
  projectId: this.projectId,
13035
13392
  sourceId: input.sourceId,
13036
13393
  materializationId: input.candidateMaterializationId,
@@ -13087,31 +13444,21 @@ class SqliteMaterializationStateReader {
13087
13444
  UNION
13088
13445
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
13089
13446
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
13090
- FROM ontology_source_rows AS rows
13091
- JOIN ontology_source_roots AS sources
13092
- ON sources.project_id = rows.project_id
13093
- AND sources.source_id = rows.source_id
13094
- AND sources.materialization_id = rows.materialization_id
13095
- AND sources.root_sort_key = rows.root_sort_key
13096
- JOIN incident_objects
13097
- ON incident_objects.object_type_id = rows.source_type_id
13098
- AND incident_objects.primary_id = rows.source_primary_id
13099
- WHERE rows.project_id = ? AND rows.entity_kind = 'link'
13100
- 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}
13101
13453
  UNION
13102
13454
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
13103
13455
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
13104
- FROM ontology_source_rows AS rows
13105
- JOIN ontology_source_roots AS sources
13106
- ON sources.project_id = rows.project_id
13107
- AND sources.source_id = rows.source_id
13108
- AND sources.materialization_id = rows.materialization_id
13109
- AND sources.root_sort_key = rows.root_sort_key
13110
- JOIN incident_objects
13111
- ON incident_objects.object_type_id = rows.target_type_id
13112
- AND incident_objects.primary_id = rows.target_primary_id
13113
- WHERE rows.project_id = ? AND rows.entity_kind = 'link'
13114
- 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}
13115
13462
  ), diff_links AS (
13116
13463
  SELECT * FROM replacement_links
13117
13464
  UNION SELECT * FROM incident_links
@@ -13261,9 +13608,7 @@ function storedSource(row) {
13261
13608
  return {
13262
13609
  source: { projectionId: row.source_id },
13263
13610
  materializationId: row.materialization_id,
13264
- root: parseJson2(row.root),
13265
- assertion: parseJson2(row.assertion),
13266
- stagingOrdinal: row.staging_ordinal
13611
+ ...sourceAssertion(row)
13267
13612
  };
13268
13613
  }
13269
13614
  function effectiveObjectSnapshot(row) {
@@ -13311,6 +13656,10 @@ function storedPoint(row) {
13311
13656
  lastCommitId: row.last_commit_id
13312
13657
  };
13313
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
+ }
13314
13663
  function linkSortExpression(alias) {
13315
13664
  const prefix = alias ? `${alias}.` : "";
13316
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)))`;
@@ -14491,51 +14840,80 @@ import {
14491
14840
  sourceEntityColumns as entityColumns,
14492
14841
  isExactStagingManifest,
14493
14842
  reconcileSourceStageRows,
14843
+ sourceAssertionPayload,
14494
14844
  sourceConflict,
14495
14845
  sourceMaterializationIdentity,
14496
14846
  sourceStageRow,
14497
- sourceStageRows,
14498
- utf8SortKey as utf8SortKey2
14847
+ sourceStageRows
14499
14848
  } from "@sixb/core/internal/ontology-storage-provider";
14500
14849
 
14501
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
+ }
14502
14889
  function cleanupSourceVersions(db, input) {
14503
14890
  let remaining = input.limit;
14891
+ const retired = `roots.project_id = ? AND roots.retired_at < ? AND versions.status = 'superseded'`;
14504
14892
  let rowsDeleted = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
14505
14893
  SELECT rows.rowid FROM ontology_source_roots AS roots
14506
- JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
14507
- CROSS JOIN ontology_source_rows AS rows
14508
- ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
14509
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
14510
- WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
14511
- AND sources.status IN ('superseded', 'abandoned')
14512
- 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
14513
14898
  LIMIT ?
14514
14899
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14515
14900
  remaining -= rowsDeleted;
14516
14901
  if (remaining > 0) {
14517
- const removed = db.query(`DELETE FROM ontology_source_roots WHERE rowid IN (
14518
- SELECT roots.rowid FROM ontology_source_roots AS roots
14519
- JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
14520
- WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
14521
- AND sources.status IN ('superseded', 'abandoned')
14522
- AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
14523
- WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
14524
- AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
14525
- 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 ?
14526
14908
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14527
14909
  rowsDeleted += removed;
14528
14910
  remaining -= removed;
14529
14911
  }
14530
- const materializationsDeleted = remaining === 0 ? 0 : db.query(`DELETE FROM ontology_sources WHERE rowid IN (
14531
- SELECT sources.rowid FROM ontology_sources AS sources
14532
- 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 < ?
14533
14915
  AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
14534
- WHERE roots.project_id = sources.project_id AND roots.source_id = sources.source_id
14535
- AND roots.materialization_id = sources.materialization_id)
14536
- AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
14537
- WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
14538
- AND rows.materialization_id = sources.materialization_id)
14916
+ WHERE roots.version_id = versions.version_id)
14539
14917
  ORDER BY terminal_at, source_id, materialization_id LIMIT ?
14540
14918
  )`).run(input.projectId, input.terminalBefore, remaining).changes;
14541
14919
  return { rowsDeleted, materializationsDeleted };
@@ -14567,7 +14945,7 @@ class SqliteOntologySourceStorage {
14567
14945
  LIMIT 1
14568
14946
  `).get(input.projectId, input.execution.projectionRunId);
14569
14947
  if (candidate) {
14570
- 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.`);
14571
14949
  }
14572
14950
  this.db.query(`
14573
14951
  INSERT INTO ontology_sources (
@@ -14593,10 +14971,10 @@ class SqliteOntologySourceStorage {
14593
14971
  throw sourceConflict(`Source materialization '${input.materializationId}' is '${manifest.status}' and cannot accept rows.`);
14594
14972
  }
14595
14973
  const rows = sourceStageRows(manifest.projection_kind, input.rows);
14596
- const existing = this.findStageRows(input, rows);
14974
+ const existing = this.findStageRows(manifest, rows);
14597
14975
  const { pending, unchanged } = reconcileSourceStageRows(rows, existing);
14598
14976
  stageSourceRoots(this.db, manifest, input);
14599
- this.insertStageRows(input, pending);
14977
+ this.insertStageRows(manifest, pending);
14600
14978
  return { inserted: pending.length, unchanged };
14601
14979
  });
14602
14980
  await yieldSqliteEventLoop();
@@ -14646,13 +15024,40 @@ class SqliteOntologySourceStorage {
14646
15024
  return row ? sourceRecord(row) : null;
14647
15025
  });
14648
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
+ }
14649
15051
  async abandon(input) {
14650
15052
  return this.runRootOperation(() => {
14651
15053
  assertProjectAndSource(input);
14652
15054
  assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken);
14653
15055
  assertTimestamp(input.abandonedAt, "Source abandonedAt", true);
14654
15056
  this.assertExecution(input);
14655
- 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;
14656
15061
  });
14657
15062
  }
14658
15063
  async cleanupTerminal(input) {
@@ -14663,6 +15068,13 @@ class SqliteOntologySourceStorage {
14663
15068
  return cleanupSourceVersions(this.db, input);
14664
15069
  });
14665
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
+ }
14666
15078
  async summarizeTerminal(input) {
14667
15079
  return this.runRootOperation(() => {
14668
15080
  assertNonblank(input.projectId, "Terminal source summary project id");
@@ -14688,19 +15100,20 @@ class SqliteOntologySourceStorage {
14688
15100
  assertSourceCandidateOwner(sourceRecord(manifest), input.execution);
14689
15101
  return this.transitionToAbandoned(manifest, input.abandonedAt);
14690
15102
  }
14691
- abandonForReclaim(input) {
15103
+ findRunCandidate(input) {
14692
15104
  const candidates = this.db.query(`
14693
15105
  SELECT * FROM ontology_sources
14694
15106
  WHERE project_id = ? AND source_id = ? AND projection_run_id = ?
14695
15107
  AND status IN ('staging', 'ready')
14696
15108
  `).all(input.projectId, input.source.projectionId, input.execution.projectionRunId);
14697
- if (candidates.some((candidate) => candidate.execution_token === input.execution.executionToken)) {
14698
- throw sourceConflict(`Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`);
14699
- }
14700
15109
  if (candidates.length > 1) {
14701
15110
  throw sourceConflict(`Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`);
14702
15111
  }
14703
- 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;
14704
15117
  }
14705
15118
  transitionToAbandoned(manifest, abandonedAt) {
14706
15119
  if (abandonedAt < manifest.created_at || manifest.ready_at !== null && abandonedAt < manifest.ready_at) {
@@ -14715,84 +15128,95 @@ class SqliteOntologySourceStorage {
14715
15128
  if (changed !== 1) {
14716
15129
  throw sourceConflict(`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`);
14717
15130
  }
14718
- this.db.query(`UPDATE ontology_source_roots SET retired_at = ?
14719
- WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND active = 0`).run(abandonedAt, manifest.project_id, manifest.source_id, manifest.materialization_id);
14720
15131
  return sourceRecord(this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id));
14721
15132
  }
14722
15133
  assertReadyState(manifest, rootCount, assertionCount) {
14723
15134
  const counts = this.db.query(`
14724
- SELECT (SELECT COUNT(*) FROM ontology_source_rows
14725
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?) AS assertions, COUNT(*) AS roots,
14726
- 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,
14727
15139
  MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
14728
15140
  FROM ontology_source_roots
14729
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14730
- `).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);
14731
15143
  if (counts.assertions !== assertionCount || counts.roots !== rootCount || counts.ordinals !== rootCount || rootCount > 0 && (counts.min_ordinal !== 0 || counts.max_ordinal !== rootCount - 1)) {
14732
15144
  throw new MaterializationValidationError4("Source ready counts do not match the staged roots and assertions.");
14733
15145
  }
14734
15146
  assertSourceRootCoverage(this.db, manifest);
14735
15147
  const invalid = this.db.query(manifest.projection_kind === "link" ? `
14736
- SELECT root_key
14737
- FROM ontology_source_rows
14738
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14739
- GROUP BY root_key
14740
- HAVING COUNT(*) <> 1 OR MIN(root_kind) <> 'link' OR MIN(entity_kind) <> 'link'
14741
- 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]'))
14742
15161
  LIMIT 1
14743
15162
  ` : `
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 MIN(root_kind) <> 'object'
14749
- OR SUM(CASE WHEN entity_kind = 'object' AND entity_key = root_key THEN 1 ELSE 0 END) <> 1
14750
- OR SUM(CASE
14751
- WHEN entity_kind = 'object' AND entity_key <> root_key THEN 1
14752
- WHEN entity_kind = 'link' AND (
14753
- source_type_id <> root_object_type_id OR source_primary_id <> root_primary_id
14754
- ) THEN 1
14755
- ELSE 0
14756
- 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)
14757
15184
  LIMIT 1
14758
- `).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
15185
+ `).get(manifest.version_id);
14759
15186
  if (invalid) {
14760
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.");
14761
15188
  }
14762
15189
  }
14763
- findStageRows(input, rows) {
15190
+ findStageRows(manifest, rows) {
14764
15191
  if (rows.length === 0)
14765
15192
  return [];
14766
- const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.rootKey)))];
15193
+ const rootKeys = [...new Set(rows.map((row) => row.rootKey))];
14767
15194
  const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))];
14768
- const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.entityKey)))];
14769
15195
  const existing = this.db.query(`
14770
- SELECT * FROM ontology_source_rows
14771
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
14772
- AND (
14773
- root_sort_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))
14774
- OR staging_ordinal IN (SELECT CAST(value AS INTEGER) FROM json_each(?))
14775
- OR entity_sort_key IN (SELECT CAST(value AS TEXT) FROM json_each(?))
14776
- )
14777
- `).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));
14778
15210
  return existing.map((row) => sourceStageRow(sourceAssertion(row)));
14779
15211
  }
14780
- insertStageRows(input, rows) {
15212
+ insertStageRows(manifest, rows) {
14781
15213
  if (rows.length === 0)
14782
15214
  return;
14783
- const payload = rows.map(({ entityKey, rootKey, row }) => {
15215
+ const payload = rows.map(({ rootKey, row }) => {
14784
15216
  const entity = entityColumns(row.assertion);
14785
- const root = entityColumns(row.root);
14786
15217
  return {
14787
- entityKind: row.assertion.kind,
14788
- entityKey,
14789
- entitySortKey: utf8SortKey2(entityKey),
14790
- rootKind: row.root.kind,
14791
15218
  rootKey,
14792
- rootSortKey: utf8SortKey2(rootKey),
14793
- stagingOrdinal: row.stagingOrdinal,
14794
- root: row.root,
14795
- assertion: row.assertion,
15219
+ entityKind: row.assertion.kind,
14796
15220
  objectTypeId: entity.objectTypeId,
14797
15221
  primaryId: entity.primaryId,
14798
15222
  sourceTypeId: entity.sourceTypeId,
@@ -14800,45 +15224,27 @@ class SqliteOntologySourceStorage {
14800
15224
  linkId: entity.linkId,
14801
15225
  targetTypeId: entity.targetTypeId,
14802
15226
  targetPrimaryId: entity.targetPrimaryId,
14803
- rootObjectTypeId: root.objectTypeId,
14804
- rootPrimaryId: root.primaryId,
14805
- rootSourceTypeId: root.sourceTypeId,
14806
- rootSourcePrimaryId: root.sourcePrimaryId,
14807
- rootLinkId: root.linkId,
14808
- rootTargetTypeId: root.targetTypeId,
14809
- rootTargetPrimaryId: root.targetPrimaryId
15227
+ payload: sourceAssertionPayload(row.assertion)
14810
15228
  };
14811
15229
  });
14812
- this.db.query(`
15230
+ const inserted = this.db.query(`
14813
15231
  WITH staged(value) AS (SELECT value FROM json_each(?))
14814
15232
  INSERT INTO ontology_source_rows (
14815
- project_id, source_id, materialization_id,
14816
- entity_kind, entity_key, entity_sort_key,
14817
- root_kind, root_key, root_sort_key, staging_ordinal,
14818
- root, assertion,
14819
- object_type_id, primary_id,
14820
- source_type_id, source_primary_id, link_id, target_type_id, target_primary_id,
14821
- root_object_type_id, root_primary_id,
14822
- root_source_type_id, root_source_primary_id, root_link_id,
14823
- 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
14824
15235
  )
14825
- SELECT
14826
- ?, ?, ?,
14827
- json_extract(value, '$.entityKind'), json_extract(value, '$.entityKey'),
14828
- json_extract(value, '$.entitySortKey'),
14829
- json_extract(value, '$.rootKind'), json_extract(value, '$.rootKey'),
14830
- json_extract(value, '$.rootSortKey'), json_extract(value, '$.stagingOrdinal'),
14831
- json(json_extract(value, '$.root')), json(json_extract(value, '$.assertion')),
15236
+ SELECT roots.id, json_extract(value, '$.entityKind'),
14832
15237
  json_extract(value, '$.objectTypeId'), json_extract(value, '$.primaryId'),
14833
15238
  json_extract(value, '$.sourceTypeId'), json_extract(value, '$.sourcePrimaryId'),
14834
15239
  json_extract(value, '$.linkId'), json_extract(value, '$.targetTypeId'),
14835
- json_extract(value, '$.targetPrimaryId'),
14836
- json_extract(value, '$.rootObjectTypeId'), json_extract(value, '$.rootPrimaryId'),
14837
- json_extract(value, '$.rootSourceTypeId'),
14838
- json_extract(value, '$.rootSourcePrimaryId'), json_extract(value, '$.rootLinkId'),
14839
- json_extract(value, '$.rootTargetTypeId'), json_extract(value, '$.rootTargetPrimaryId')
15240
+ json_extract(value, '$.targetPrimaryId'), json_extract(value, '$.payload')
14840
15241
  FROM staged
14841
- `).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
+ }
14842
15248
  }
14843
15249
  assertExecution(input, identity) {
14844
15250
  assertProjectionExecution(this.db, {
@@ -15105,6 +15511,8 @@ function queryLatestRunsByOwnerId(input) {
15105
15511
  return [];
15106
15512
  }
15107
15513
  const placeholders = ownerIds.map(() => "?").join(", ");
15514
+ const statusFilter = input.excludedStatus === undefined ? "" : " AND status <> ?";
15515
+ const statusArgs = input.excludedStatus === undefined ? [] : [input.excludedStatus];
15108
15516
  const orderColumn = input.tableName === "sync_runs" || input.tableName === "pipeline_runs" || input.tableName === "projection_runs" ? "COALESCE(started_at, queued_at)" : "started_at";
15109
15517
  const rows = input.db.query(`
15110
15518
  SELECT *
@@ -15116,10 +15524,10 @@ function queryLatestRunsByOwnerId(input) {
15116
15524
  ORDER BY ${orderColumn} DESC, id DESC
15117
15525
  ) AS sixb_latest_rank
15118
15526
  FROM ${input.tableName}
15119
- WHERE project_id = ? AND ${input.ownerColumn} IN (${placeholders})
15527
+ WHERE project_id = ? AND ${input.ownerColumn} IN (${placeholders})${statusFilter}
15120
15528
  )
15121
15529
  WHERE sixb_latest_rank = 1
15122
- `).all(input.projectId, ...ownerIds);
15530
+ `).all(input.projectId, ...ownerIds, ...statusArgs);
15123
15531
  const latestByOwnerId = new Map(rows.map((row) => [input.ownerIdFor(row), row]));
15124
15532
  return ownerIds.flatMap((ownerId) => {
15125
15533
  const row = latestByOwnerId.get(ownerId);
@@ -15465,12 +15873,14 @@ import {
15465
15873
  failProjectionRunEnqueue,
15466
15874
  immutableDatasetVersionConflict,
15467
15875
  mergeProjectionRunProgress,
15876
+ parseProjectionAttemptFailure,
15468
15877
  planProjectionRunFinish,
15469
15878
  planProjectionRunReclaim,
15470
15879
  projectionRunNotFound,
15471
15880
  publicProjectionRunRecord,
15472
15881
  requireTelemetryProjectionRun,
15473
15882
  restoreProjectionRun,
15883
+ SUPERSEDING_PROJECTION_RUN_STATUSES,
15474
15884
  staleProjectionRunExecution
15475
15885
  } from "@sixb/core/internal/projection-run-storage-provider";
15476
15886
  import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage";
@@ -15552,7 +15962,7 @@ class SqliteProjectionRunStorage {
15552
15962
  const result = this.db.query(`
15553
15963
  UPDATE projection_runs
15554
15964
  SET status = 'running', started_at = COALESCE(started_at, ?), finished_at = NULL,
15555
- attempt = ?, execution_token = ?, error = NULL
15965
+ attempt = ?, execution_token = ?
15556
15966
  WHERE project_id = ? AND id = ?
15557
15967
  AND (
15558
15968
  (status = 'queued' AND execution_token IS NULL)
@@ -15596,6 +16006,20 @@ class SqliteProjectionRunStorage {
15596
16006
  return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id));
15597
16007
  });
15598
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
+ }
15599
16023
  async finish(input) {
15600
16024
  return runImmediateTransaction(this.db, () => {
15601
16025
  const existingRow = this.requireMaterializationExecution(input);
@@ -15733,6 +16157,21 @@ class SqliteProjectionRunStorage {
15733
16157
  const runs = rows.map(rowToProjectionRunRecord);
15734
16158
  return { runs, hasMore: offset + runs.length < totalRow.count, total: totalRow.count };
15735
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
+ }
15736
16175
  async listLatestByProjectionIds(input) {
15737
16176
  const rows = queryLatestRunsByOwnerId({
15738
16177
  db: this.db,
@@ -15740,7 +16179,8 @@ class SqliteProjectionRunStorage {
15740
16179
  ownerColumn: "projection_id",
15741
16180
  ownerIds: input.projectionIds,
15742
16181
  projectId: input.projectId,
15743
- ownerIdFor: (row) => row.projection_id
16182
+ ownerIdFor: (row) => row.projection_id,
16183
+ excludedStatus: "superseded"
15744
16184
  });
15745
16185
  return { runs: rows.map(rowToProjectionRunRecord) };
15746
16186
  }
@@ -17937,4 +18377,4 @@ export {
17937
18377
  migrateSqliteStorage
17938
18378
  };
17939
18379
 
17940
- //# debugId=A346535172FAF7A764756E2164756E21
18380
+ //# debugId=1D3AEE45CA656B6464756E2164756E21