@sixb/sqlite 0.1.9 → 0.1.11

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 (29) hide show
  1. package/dist/index.js +449 -195
  2. package/dist/index.js.map +13 -11
  3. package/dist/migrations.d.ts.map +1 -1
  4. package/dist/ontology-storage/materialization-state.d.ts +2 -1
  5. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  6. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  7. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  8. package/dist/ontology-storage/shared.d.ts +2 -0
  9. package/dist/ontology-storage/shared.d.ts.map +1 -1
  10. package/dist/ontology-storage/source-cleanup.d.ts +4 -0
  11. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
  12. package/dist/ontology-storage/source-roots.d.ts +27 -0
  13. package/dist/ontology-storage/source-roots.d.ts.map +1 -0
  14. package/dist/ontology-storage/sources.d.ts.map +1 -1
  15. package/dist/projection-run-storage.d.ts.map +1 -1
  16. package/package.json +3 -3
  17. package/src/connector-connection-storage.ts +13 -3
  18. package/src/migrations/038-projection-source-roots.sql +44 -0
  19. package/src/migrations/039-connector-optional-pkce.sql +52 -0
  20. package/src/migrations/040-outbox-publication-order.sql +8 -0
  21. package/src/migrations.ts +13 -0
  22. package/src/ontology-storage/materialization-state.ts +56 -91
  23. package/src/ontology-storage/materializations.ts +7 -1
  24. package/src/ontology-storage/outbox.ts +33 -16
  25. package/src/ontology-storage/shared.ts +10 -0
  26. package/src/ontology-storage/source-cleanup.ts +59 -0
  27. package/src/ontology-storage/source-roots.ts +230 -0
  28. package/src/ontology-storage/sources.ts +25 -78
  29. package/src/projection-run-storage.ts +11 -3
package/dist/index.js CHANGED
@@ -3932,6 +3932,64 @@ DROP TABLE executions;
3932
3932
  ALTER TABLE executions_v2 RENAME TO executions;
3933
3933
  `;
3934
3934
 
3935
+ // src/migrations/038-projection-source-roots.sql
3936
+ var _038_projection_source_roots_default = `ALTER TABLE ontology_sources ADD COLUMN base_materialization_id TEXT;
3937
+ ALTER TABLE ontology_sources ADD COLUMN base_commit_id TEXT
3938
+ CHECK ((base_materialization_id IS NULL) = (base_commit_id IS NULL));
3939
+
3940
+ CREATE TABLE ontology_source_roots (
3941
+ project_id TEXT NOT NULL,
3942
+ source_id TEXT NOT NULL,
3943
+ materialization_id TEXT NOT NULL,
3944
+ root_sort_key TEXT NOT NULL,
3945
+ root_kind TEXT NOT NULL CHECK (root_kind IN ('object', 'link')),
3946
+ root_key TEXT NOT NULL CHECK (json_valid(root_key)),
3947
+ root TEXT NOT NULL CHECK (json_valid(root)),
3948
+ staging_ordinal INTEGER NOT NULL CHECK (staging_ordinal >= 0),
3949
+ deleted INTEGER NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
3950
+ active INTEGER NOT NULL DEFAULT 0 CHECK (active IN (0, 1)),
3951
+ retired_at TEXT,
3952
+ PRIMARY KEY (project_id, source_id, materialization_id, root_sort_key),
3953
+ UNIQUE (project_id, source_id, materialization_id, staging_ordinal),
3954
+ FOREIGN KEY (project_id, source_id, materialization_id)
3955
+ REFERENCES ontology_sources(project_id, source_id, materialization_id) ON DELETE RESTRICT,
3956
+ CHECK (active = 0 OR (deleted = 0 AND retired_at IS NULL))
3957
+ );
3958
+ CREATE UNIQUE INDEX idx_ontology_source_roots_active
3959
+ ON ontology_source_roots (project_id, source_id, root_sort_key) WHERE active = 1;
3960
+ CREATE INDEX idx_ontology_source_roots_lookup
3961
+ ON ontology_source_roots (project_id, root_sort_key, source_id) WHERE active = 1;
3962
+ CREATE INDEX idx_ontology_source_roots_cleanup
3963
+ ON ontology_source_roots (project_id, retired_at, source_id, materialization_id, root_sort_key)
3964
+ WHERE active = 0 AND retired_at IS NOT NULL;
3965
+
3966
+ INSERT INTO ontology_source_roots (
3967
+ project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root,
3968
+ staging_ordinal, active, retired_at
3969
+ )
3970
+ SELECT DISTINCT rows.project_id, rows.source_id, rows.materialization_id,
3971
+ rows.root_sort_key, rows.root_kind, rows.root_key, rows.root, rows.staging_ordinal,
3972
+ sources.status = 'active', sources.terminal_at
3973
+ FROM ontology_source_rows AS rows
3974
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id);
3975
+
3976
+ ANALYZE ontology_source_roots;
3977
+
3978
+ ALTER TABLE projection_runs ADD COLUMN source_changes_read INTEGER
3979
+ CHECK (source_changes_read IS NULL OR source_changes_read >= 0);
3980
+ `;
3981
+
3982
+ // src/migrations/040-outbox-publication-order.sql
3983
+ var _040_outbox_publication_order_default = `-- Match the bounded claim order without sorting the entire unpublished backlog.
3984
+ -- Cover every selection predicate so new databases also benefit before ANALYZE.
3985
+ CREATE INDEX idx_ontology_outbox_publication_order
3986
+ ON ontology_outbox(
3987
+ project_id, published_at, created_at, commit_id, commit_ordinal,
3988
+ available_at, lease_expires_at
3989
+ )
3990
+ WHERE published_at IS NULL;
3991
+ `;
3992
+
3935
3993
  // src/migrations/037-execution-requester-groups.sql
3936
3994
  var _037_execution_requester_groups_default = `ALTER TABLE executions ADD COLUMN requester_group_ids TEXT NOT NULL DEFAULT '[]';
3937
3995
 
@@ -3959,6 +4017,61 @@ ALTER TABLE workflow_runs DROP COLUMN requester_group_ids;
3959
4017
  ALTER TABLE agent_runs DROP COLUMN requester_group_ids;
3960
4018
  `;
3961
4019
 
4020
+ // src/migrations/039-connector-optional-pkce.sql
4021
+ var _039_connector_optional_pkce_default = `-- Rebuild to remove NOT NULL while preserving existing attempts and their constraints.
4022
+ CREATE TABLE connector_authorization_attempts_optional_pkce (
4023
+ project_id TEXT NOT NULL,
4024
+ connector_id TEXT NOT NULL,
4025
+ id TEXT NOT NULL,
4026
+ slot TEXT NOT NULL,
4027
+ initiated_by_execution_id TEXT NOT NULL,
4028
+ state_hash TEXT NOT NULL,
4029
+ code_verifier TEXT CHECK (
4030
+ code_verifier IS NULL
4031
+ OR (json_valid(code_verifier) AND json_type(code_verifier) = 'object')
4032
+ ),
4033
+ redirect_uri TEXT NOT NULL,
4034
+ connection_run_id TEXT,
4035
+ return_to TEXT,
4036
+ callback_binding_hash TEXT,
4037
+ reauthorization_id TEXT,
4038
+ reauthorization_revision INTEGER CHECK (
4039
+ reauthorization_revision IS NULL OR reauthorization_revision >= 0
4040
+ ),
4041
+ reauthorization_connection_ids TEXT CHECK (
4042
+ reauthorization_connection_ids IS NULL
4043
+ OR (json_valid(reauthorization_connection_ids) AND json_type(reauthorization_connection_ids) = 'array')
4044
+ ),
4045
+ created_at TEXT NOT NULL,
4046
+ expires_at TEXT NOT NULL,
4047
+ PRIMARY KEY (project_id, connector_id, id),
4048
+ UNIQUE (project_id, id),
4049
+ UNIQUE (project_id, connector_id, connection_run_id),
4050
+ FOREIGN KEY (project_id, connector_id, connection_run_id)
4051
+ REFERENCES connector_connection_runs (project_id, connector_id, id)
4052
+ ON DELETE RESTRICT,
4053
+ CHECK (
4054
+ (connection_run_id IS NULL AND return_to IS NULL AND callback_binding_hash IS NULL)
4055
+ OR
4056
+ (connection_run_id IS NOT NULL AND return_to IS NOT NULL AND callback_binding_hash IS NOT NULL)
4057
+ ),
4058
+ CHECK (
4059
+ (reauthorization_id IS NULL AND reauthorization_revision IS NULL AND reauthorization_connection_ids IS NULL)
4060
+ OR
4061
+ (reauthorization_id IS NOT NULL AND reauthorization_revision IS NOT NULL AND reauthorization_connection_ids IS NOT NULL)
4062
+ )
4063
+ );
4064
+
4065
+ INSERT INTO connector_authorization_attempts_optional_pkce
4066
+ SELECT * FROM connector_authorization_attempts;
4067
+
4068
+ DROP TABLE connector_authorization_attempts;
4069
+ ALTER TABLE connector_authorization_attempts_optional_pkce RENAME TO connector_authorization_attempts;
4070
+
4071
+ CREATE INDEX idx_connector_attempts_expiry
4072
+ ON connector_authorization_attempts (project_id, connector_id, expires_at);
4073
+ `;
4074
+
3962
4075
  // src/migrations.ts
3963
4076
  var MIGRATIONS_TABLE_SQL = `
3964
4077
  CREATE TABLE IF NOT EXISTS sixb_migrations (
@@ -4030,7 +4143,10 @@ var sqliteStorageMigrations = defineMigrations({
4030
4143
  sqliteSql("034-device-authorizations", _034_device_authorizations_default),
4031
4144
  sqliteSql("035-share-grants", _035_share_grants_default),
4032
4145
  sqliteSql("036-share-sessions", _036_share_sessions_default),
4033
- sqliteSql("037-execution-requester-groups", _037_execution_requester_groups_default)
4146
+ sqliteSql("037-execution-requester-groups", _037_execution_requester_groups_default),
4147
+ sqliteSql("038-projection-source-roots", _038_projection_source_roots_default),
4148
+ sqliteSql("039-connector-optional-pkce", _039_connector_optional_pkce_default),
4149
+ sqliteSql("040-outbox-publication-order", _040_outbox_publication_order_default)
4034
4150
  ]
4035
4151
  });
4036
4152
  function sqliteStoragePath(basePath) {
@@ -9014,7 +9130,7 @@ class SqliteConnectorConnectionPersistence {
9014
9130
  callback_binding_hash, reauthorization_id, reauthorization_revision,
9015
9131
  reauthorization_connection_ids, created_at, expires_at
9016
9132
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9017
- `).run(record.projectId, record.connectorId, record.id, record.slot, record.initiatedByExecutionId, record.stateHash, json(record.codeVerifier), record.redirectUri, record.connectionRunId ?? null, record.returnTo ?? null, record.callbackBindingHash ?? null, record.reauthorizationId ?? null, record.reauthorizationRevision ?? null, record.reauthorizationConnectionIds ? json(record.reauthorizationConnectionIds) : null, iso(record.createdAt), iso(record.expiresAt));
9133
+ `).run(record.projectId, record.connectorId, record.id, record.slot, record.initiatedByExecutionId, record.stateHash, record.codeVerifier === undefined ? null : json(record.codeVerifier), record.redirectUri, record.connectionRunId ?? null, record.returnTo ?? null, record.callbackBindingHash ?? null, record.reauthorizationId ?? null, record.reauthorizationRevision ?? null, record.reauthorizationConnectionIds ? json(record.reauthorizationConnectionIds) : null, iso(record.createdAt), iso(record.expiresAt));
9018
9134
  return true;
9019
9135
  } catch (error) {
9020
9136
  if (isUniqueConstraintError2(error))
@@ -9048,6 +9164,14 @@ class SqliteConnectorConnectionPersistence {
9048
9164
  throw error;
9049
9165
  }
9050
9166
  }
9167
+ async listPendingConnectionRuns() {
9168
+ if (!this.scope.connectorId)
9169
+ return [];
9170
+ const rows = this.db.query(`SELECT * FROM connector_connection_runs
9171
+ WHERE project_id = ? AND connector_id = ?
9172
+ AND (status = 'running' OR (status = 'waiting' AND waiting_for = 'account_selection'))`).all(this.scope.projectId, this.scope.connectorId);
9173
+ return rows.map(connectionRunFromRow);
9174
+ }
9051
9175
  async getConnectionRun(id) {
9052
9176
  const row = this.getById("connector_connection_runs", id);
9053
9177
  return row ? connectionRunFromRow(row) : null;
@@ -9195,7 +9319,7 @@ function authorizationAttemptFromRow(row) {
9195
9319
  slot: row.slot,
9196
9320
  initiatedByExecutionId: row.initiated_by_execution_id,
9197
9321
  stateHash: row.state_hash,
9198
- codeVerifier: parseJson(row.code_verifier),
9322
+ ...row.code_verifier === null ? {} : { codeVerifier: parseJson(row.code_verifier) },
9199
9323
  redirectUri: row.redirect_uri,
9200
9324
  ...row.connection_run_id === null ? {} : { connectionRunId: row.connection_run_id },
9201
9325
  ...row.return_to === null ? {} : { returnTo: row.return_to },
@@ -11387,6 +11511,12 @@ function sourceRecord(row) {
11387
11511
  projectionRevision: row.projection_revision,
11388
11512
  ownershipHash: row.ownership_hash,
11389
11513
  ontologyRevision: row.ontology_revision,
11514
+ ...row.base_materialization_id === null ? {} : {
11515
+ base: {
11516
+ materializationId: row.base_materialization_id,
11517
+ lastCommitId: row.base_commit_id
11518
+ }
11519
+ },
11390
11520
  rootCount: row.root_count,
11391
11521
  assertionCount: row.assertion_count,
11392
11522
  createdAt: row.created_at,
@@ -11558,7 +11688,7 @@ import {
11558
11688
  linkRefKey as linkRefKey3,
11559
11689
  linkRefSortKey,
11560
11690
  linkScopeSortKey as linkScopeSortKey2,
11561
- MaterializationConflictError as MaterializationConflictError5,
11691
+ MaterializationConflictError as MaterializationConflictError6,
11562
11692
  objectRefKey as objectRefKey3,
11563
11693
  objectRefSortKey,
11564
11694
  telemetryPointKey as telemetryPointKey2,
@@ -11578,7 +11708,7 @@ import {
11578
11708
 
11579
11709
  // src/ontology-storage/materialization-session.ts
11580
11710
  import {
11581
- MaterializationConflictError as MaterializationConflictError3
11711
+ MaterializationConflictError as MaterializationConflictError4
11582
11712
  } from "@sixb/core/internal/materialization";
11583
11713
  import {
11584
11714
  correlateMaterializationChunk,
@@ -11593,9 +11723,9 @@ import {
11593
11723
  import {
11594
11724
  linkRefKey,
11595
11725
  linkScopeSortKey,
11596
- MaterializationConflictError as MaterializationConflictError2,
11726
+ MaterializationConflictError as MaterializationConflictError3,
11597
11727
  objectRefKey,
11598
- projectionEntityKey,
11728
+ projectionEntityKey as projectionEntityKey2,
11599
11729
  telemetryPointSortKey
11600
11730
  } from "@sixb/core/internal/materialization";
11601
11731
  import {
@@ -11604,6 +11734,132 @@ import {
11604
11734
  linkSlotOverrideValue,
11605
11735
  startScopeAccumulator
11606
11736
  } from "@sixb/core/internal/ontology-storage-provider";
11737
+
11738
+ // src/ontology-storage/source-roots.ts
11739
+ import {
11740
+ MaterializationConflictError as MaterializationConflictError2,
11741
+ MaterializationValidationError,
11742
+ projectionEntityKey
11743
+ } from "@sixb/core/internal/materialization";
11744
+ import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider";
11745
+ function replacementSourceRows(input) {
11746
+ const roots = input.incremental ? `ontology_source_roots AS changed
11747
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = changed.project_id
11748
+ 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";
11749
+ const selected = input.incremental ? "changed" : "roots";
11750
+ return {
11751
+ sql: `SELECT * FROM ontology_source_rows WHERE project_id = ? AND source_id = ? AND materialization_id = ?
11752
+ UNION ALL
11753
+ SELECT rows.* FROM ${roots}
11754
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id
11755
+ AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
11756
+ AND rows.root_sort_key = roots.root_sort_key
11757
+ WHERE ${selected}.project_id = ? AND ${selected}.source_id = ?
11758
+ AND ${input.incremental ? "changed.materialization_id = ?" : "roots.active = 1"}`,
11759
+ values: [
11760
+ input.projectId,
11761
+ input.sourceId,
11762
+ input.materializationId,
11763
+ input.projectId,
11764
+ input.sourceId,
11765
+ ...input.incremental ? [input.materializationId] : []
11766
+ ]
11767
+ };
11768
+ }
11769
+ function stageSourceRoots(db, manifest, input) {
11770
+ const roots = sourceStageRoots(manifest.projection_kind, input);
11771
+ if (!manifest.base_materialization_id && roots.some((root) => root.deleted)) {
11772
+ throw new MaterializationValidationError("Root deletions require a source delta base.");
11773
+ }
11774
+ const existing = db.query(`SELECT staging_ordinal, deleted FROM ontology_source_roots
11775
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND root_sort_key = ?`);
11776
+ const insert = db.query(`INSERT INTO ontology_source_roots (
11777
+ project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
11778
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING root_sort_key`);
11779
+ for (const root of roots) {
11780
+ 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));
11781
+ if (inserted)
11782
+ continue;
11783
+ const previous = existing.get(input.projectId, input.source.projectionId, input.materializationId, root.sortKey);
11784
+ if (!previous || previous.staging_ordinal !== root.stagingOrdinal || Boolean(previous.deleted) !== root.deleted) {
11785
+ throw new MaterializationValidationError("Source materialization repeats root or stream ordinal with different content.");
11786
+ }
11787
+ }
11788
+ }
11789
+ function activateSourceRoots(db, projectId, candidate, activation) {
11790
+ if (candidate.base_materialization_id !== null && (candidate.base_materialization_id !== activation.expected.activeMaterializationId || candidate.base_commit_id !== activation.expected.lastCommitId))
11791
+ throw new MaterializationConflictError2("projection-fence", "Source delta base changed before activation.");
11792
+ if (candidate.base_materialization_id !== null && candidate.root_count === 0)
11793
+ return;
11794
+ const scope = [projectId, activation.source.projectionId, activation.materializationId];
11795
+ db.query(`UPDATE ontology_source_roots AS roots SET active = 0, retired_at = ?
11796
+ WHERE project_id = ? AND source_id = ? AND active = 1
11797
+ ${candidate.base_materialization_id === null ? "" : `AND root_sort_key IN (
11798
+ SELECT root_sort_key FROM ontology_source_roots
11799
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?)`}`).run(activation.updatedAt, projectId, activation.source.projectionId, ...candidate.base_materialization_id === null ? [] : [...scope]);
11800
+ db.query(`UPDATE ontology_source_roots SET active = CASE WHEN deleted = 1 THEN 0 ELSE 1 END,
11801
+ retired_at = CASE WHEN deleted = 1 THEN ? ELSE NULL END
11802
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?`).run(activation.updatedAt, ...scope);
11803
+ }
11804
+ function assertSourceRootCoverage(db, manifest) {
11805
+ const invalid = db.query(`SELECT 1 FROM ontology_source_roots AS roots
11806
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?
11807
+ AND roots.deleted = EXISTS (
11808
+ SELECT 1 FROM ontology_source_rows AS rows
11809
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
11810
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
11811
+ ) LIMIT 1`).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
11812
+ if (invalid)
11813
+ throw new MaterializationValidationError("Source roots must have complete assertions or an explicit deletion, never both.");
11814
+ }
11815
+ function activeSourceRows(db, projectId, refs) {
11816
+ const keys = refs.flatMap((entity) => {
11817
+ const entitySortKey = utf8SortKey(projectionEntityKey(entity));
11818
+ const roots = entity.kind === "object" ? [entitySortKey] : [
11819
+ entitySortKey,
11820
+ utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source }))
11821
+ ];
11822
+ return roots.map((rootSortKey) => ({ rootSortKey, entitySortKey }));
11823
+ });
11824
+ return db.query(`WITH requested AS (
11825
+ SELECT DISTINCT json_extract(value, '$.rootSortKey') AS root_sort_key,
11826
+ json_extract(value, '$.entitySortKey') AS entity_sort_key FROM json_each(?)
11827
+ ) SELECT rows.* FROM requested
11828
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ?
11829
+ AND roots.active = 1 AND roots.root_sort_key = requested.root_sort_key
11830
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
11831
+ AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
11832
+ AND rows.root_sort_key = roots.root_sort_key`).all(canonicalJson(keys), projectId);
11833
+ }
11834
+ function replacementAssertionRows(db, input) {
11835
+ const keys = input.refs.map((ref) => ({
11836
+ entityKind: ref.kind,
11837
+ entityKey: projectionEntityKey(ref),
11838
+ rootKeys: ref.kind === "object" ? [utf8SortKey(projectionEntityKey(ref))] : [
11839
+ utf8SortKey(projectionEntityKey(ref)),
11840
+ utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source }))
11841
+ ]
11842
+ }));
11843
+ const previous = !input.includePrevious ? "" : `UNION ALL
11844
+ SELECT rows.* FROM requested_entities AS requested
11845
+ CROSS JOIN ontology_source_roots AS roots ON roots.project_id = ? AND roots.source_id = ?
11846
+ AND roots.active = 1 AND roots.root_sort_key IN (SELECT value FROM json_each(requested.root_keys))
11847
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
11848
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
11849
+ AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
11850
+ ${!input.incremental ? "" : `WHERE EXISTS (SELECT 1 FROM ontology_source_roots AS changed
11851
+ WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
11852
+ AND changed.materialization_id = ? AND changed.root_sort_key = roots.root_sort_key)`}`;
11853
+ return db.query(`WITH requested_entities AS (
11854
+ SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
11855
+ json_extract(value, '$.entityKey') AS entity_key, json_extract(value, '$.rootKeys') AS root_keys FROM json_each(?)
11856
+ ) SELECT rows.* FROM requested_entities AS requested
11857
+ CROSS JOIN ontology_source_rows AS rows ON rows.project_id = ? AND rows.source_id = ?
11858
+ AND rows.materialization_id = ? AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
11859
+ ${previous}`).all(canonicalJson(keys), input.projectId, input.sourceId, input.materializationId, ...input.includePrevious ? [input.projectId, input.sourceId, ...input.incremental ? [input.materializationId] : []] : []);
11860
+ }
11861
+
11862
+ // src/ontology-storage/materialization-state.ts
11607
11863
  var SQLITE_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work";
11608
11864
  var SQLITE_REPLACEMENT_WORK_TABLE = "ontology_replacement_work";
11609
11865
  function linkScopeRequestJson(scopes) {
@@ -11640,18 +11896,7 @@ class SqliteMaterializationStateReader {
11640
11896
  ON objects.project_id = ?
11641
11897
  AND objects.object_type_id = requested.object_type_id
11642
11898
  AND objects.primary_id = requested.primary_id`).all(requested, this.projectId);
11643
- const sourceRows = this.db.query(`WITH requested AS (${requestedObjects})
11644
- SELECT rows.* FROM requested
11645
- CROSS JOIN ontology_source_rows AS rows
11646
- ON rows.project_id = ?
11647
- AND rows.entity_kind = 'object'
11648
- AND rows.object_type_id = requested.object_type_id
11649
- AND rows.primary_id = requested.primary_id
11650
- JOIN ontology_sources AS sources
11651
- ON sources.project_id = rows.project_id
11652
- AND sources.source_id = rows.source_id
11653
- AND sources.materialization_id = rows.materialization_id
11654
- WHERE sources.status = 'active'`).all(requested, this.projectId);
11899
+ const sourceRows = activeSourceRows(this.db, this.projectId, refs.map((ref) => ({ kind: "object", ref })));
11655
11900
  const overrideRows = this.db.query(`WITH requested AS (${requestedObjects})
11656
11901
  SELECT overrides.* FROM requested
11657
11902
  CROSS JOIN ontology_object_overrides AS overrides
@@ -11690,7 +11935,7 @@ class SqliteMaterializationStateReader {
11690
11935
  const effectiveRow = effective.get(key);
11691
11936
  return {
11692
11937
  ref: structuredClone(ref),
11693
- source: sourceObject(sources.get(projectionEntityKey({ kind: "object", ref }))),
11938
+ source: sourceObject(sources.get(projectionEntityKey2({ kind: "object", ref }))),
11694
11939
  override: overrides.get(key) ?? null,
11695
11940
  effective: effectiveRow ? effectiveObjectSnapshot(effectiveRow) : null,
11696
11941
  latestTelemetry: telemetry.get(key) ?? []
@@ -11735,21 +11980,7 @@ class SqliteMaterializationStateReader {
11735
11980
  AND links.link_id = requested.link_id
11736
11981
  AND links.target_type_id = requested.target_type_id
11737
11982
  AND links.target_id = requested.target_id`).all(requested, this.projectId);
11738
- const sourceRows = this.db.query(`WITH requested AS (${sourceRequest})
11739
- SELECT rows.* FROM requested
11740
- CROSS JOIN ontology_source_rows AS rows
11741
- ON rows.project_id = ?
11742
- AND rows.entity_kind = 'link'
11743
- AND rows.source_type_id = requested.source_type_id
11744
- AND rows.source_primary_id = requested.source_primary_id
11745
- AND rows.link_id = requested.link_id
11746
- AND rows.target_type_id = requested.target_type_id
11747
- AND rows.target_primary_id = requested.target_primary_id
11748
- JOIN ontology_sources AS sources
11749
- ON sources.project_id = rows.project_id
11750
- AND sources.source_id = rows.source_id
11751
- AND sources.materialization_id = rows.materialization_id
11752
- WHERE sources.status = 'active'`).all(requested, this.projectId);
11983
+ const sourceRows = activeSourceRows(this.db, this.projectId, refs.map((ref) => ({ kind: "link", ref })));
11753
11984
  const overrideRows = this.db.query(`WITH requested AS (${sourceRequest})
11754
11985
  SELECT overrides.* FROM requested
11755
11986
  CROSS JOIN ontology_link_overrides AS overrides
@@ -11780,7 +12011,7 @@ class SqliteMaterializationStateReader {
11780
12011
  const effectiveRow = effective.get(key);
11781
12012
  return {
11782
12013
  ref: structuredClone(ref),
11783
- source: sourceLink(sources.get(projectionEntityKey({ kind: "link", ref }))),
12014
+ source: sourceLink(sources.get(projectionEntityKey2({ kind: "link", ref }))),
11784
12015
  override: overrides.get(key) ?? null,
11785
12016
  slotOverride: slotOverrides.get(linkScopeSortKey(ref.source, ref.linkId)) ?? null,
11786
12017
  effective: effectiveRow ? effectiveLinkSnapshot(effectiveRow) : null
@@ -11845,11 +12076,12 @@ class SqliteMaterializationStateReader {
11845
12076
  AND rows.source_type_id = requested.source_type_id
11846
12077
  AND rows.source_primary_id = requested.source_primary_id
11847
12078
  AND rows.link_id = requested.link_id
11848
- JOIN ontology_sources AS sources
12079
+ JOIN ontology_source_roots AS sources
11849
12080
  ON sources.project_id = rows.project_id
11850
12081
  AND sources.source_id = rows.source_id
11851
12082
  AND sources.materialization_id = rows.materialization_id
11852
- WHERE sources.status = 'active'`).all(requestedJson, this.projectId);
12083
+ AND sources.root_sort_key = rows.root_sort_key
12084
+ WHERE sources.active = 1`).all(requestedJson, this.projectId);
11853
12085
  const slotOverrideRows = this.db.query(`WITH requested AS (
11854
12086
  SELECT DISTINCT
11855
12087
  json_extract(value, '$.sourceTypeId') AS source_type_id,
@@ -11907,7 +12139,7 @@ class SqliteMaterializationStateReader {
11907
12139
  for (const row of rows) {
11908
12140
  const accumulator = accumulators.get(row.scope_sort_key);
11909
12141
  if (!accumulator) {
11910
- throw new MaterializationConflictError2("effective-state", `Unexpected link scope '${row.scope_sort_key}' returned by storage.`);
12142
+ throw new MaterializationConflictError3("effective-state", `Unexpected link scope '${row.scope_sort_key}' returned by storage.`);
11911
12143
  }
11912
12144
  const snapshot = effectiveLinkSnapshot(row);
11913
12145
  appendScopeSnapshot(accumulator, snapshot);
@@ -11938,11 +12170,12 @@ class SqliteMaterializationStateReader {
11938
12170
  SELECT rows.source_type_id, rows.source_primary_id, rows.link_id,
11939
12171
  rows.target_type_id, rows.target_primary_id
11940
12172
  FROM ontology_source_rows AS rows
11941
- JOIN ontology_sources AS sources
12173
+ JOIN ontology_source_roots AS sources
11942
12174
  ON sources.project_id = rows.project_id
11943
12175
  AND sources.source_id = rows.source_id
11944
12176
  AND sources.materialization_id = rows.materialization_id
11945
- WHERE rows.project_id = ? AND rows.entity_kind = 'link' AND sources.status = 'active'
12177
+ AND sources.root_sort_key = rows.root_sort_key
12178
+ WHERE rows.project_id = ? AND rows.entity_kind = 'link' AND sources.active = 1
11946
12179
  ), selected AS (
11947
12180
  SELECT DISTINCT links.*,
11948
12181
  ${linkSortExpression("links")} AS sort_key
@@ -11985,19 +12218,19 @@ class SqliteMaterializationStateReader {
11985
12218
  const candidates = this.replacementSources(sourceId, [candidateMaterializationId], refs.map((ref) => ({ kind: "object", ref })));
11986
12219
  return base.map((state) => ({
11987
12220
  ref: state.ref,
11988
- candidateSource: sourceObject(candidates.byMaterialization.get(candidateMaterializationId)?.get(projectionEntityKey({ kind: "object", ref: state.ref }))),
12221
+ candidateSource: sourceObject(candidates.byMaterialization.get(candidateMaterializationId)?.get(projectionEntityKey2({ kind: "object", ref: state.ref }))),
11989
12222
  override: state.override,
11990
12223
  effective: state.effective,
11991
12224
  latestTelemetry: state.latestTelemetry
11992
12225
  }));
11993
12226
  }
11994
- replacementLinkStates(sourceId, candidateMaterializationId, materializationIds, identities) {
12227
+ replacementLinkStates(sourceId, candidateMaterializationId, materializationIds, identities, incremental = false) {
11995
12228
  const refs = identities.map((identity) => identity.ref);
11996
12229
  const base = this.linkStates(refs);
11997
- const replacements = this.replacementSources(sourceId, materializationIds, refs.map((ref) => ({ kind: "link", ref })));
12230
+ const replacements = this.replacementSources(sourceId, materializationIds, refs.map((ref) => ({ kind: "link", ref })), incremental);
11998
12231
  const candidate = replacements.byMaterialization.get(candidateMaterializationId);
11999
12232
  return base.map((state, index) => {
12000
- const key = projectionEntityKey({ kind: "link", ref: state.ref });
12233
+ const key = projectionEntityKey2({ kind: "link", ref: state.ref });
12001
12234
  return {
12002
12235
  ref: state.ref,
12003
12236
  candidateSource: replacements.owned.has(key) ? sourceLink(candidate?.get(key)) : state.source,
@@ -12021,32 +12254,23 @@ class SqliteMaterializationStateReader {
12021
12254
  `).get(this.projectId, ref.source.objectTypeId, ref.source.primaryId, ref.linkId, ref.target.objectTypeId, ref.target.primaryId);
12022
12255
  return row ? row.last_commit_id : undefined;
12023
12256
  }
12024
- replacementSources(sourceId, materializationIds, refs) {
12257
+ replacementSources(sourceId, materializationIds, refs, incremental = false) {
12025
12258
  if (materializationIds.length === 0 || refs.length === 0) {
12026
12259
  return { owned: new Set, byMaterialization: new Map };
12027
12260
  }
12028
- const requestedEntities = canonicalJson(refs.map((ref) => ({ entityKind: ref.kind, entityKey: projectionEntityKey(ref) })));
12029
- const requestedMaterializations = canonicalJson([...new Set(materializationIds)]);
12030
- const rows = this.db.query(`WITH requested_entities AS (
12031
- SELECT DISTINCT json_extract(value, '$.entityKind') AS entity_kind,
12032
- json_extract(value, '$.entityKey') AS entity_key
12033
- FROM json_each(?)
12034
- ), requested_materializations AS (
12035
- SELECT value AS materialization_id FROM json_each(?)
12036
- )
12037
- SELECT rows.* FROM requested_materializations
12038
- CROSS JOIN requested_entities
12039
- CROSS JOIN ontology_source_rows AS rows
12040
- ON rows.project_id = ?
12041
- AND rows.source_id = ?
12042
- AND rows.materialization_id = requested_materializations.materialization_id
12043
- AND rows.entity_kind = requested_entities.entity_kind
12044
- AND rows.entity_key = requested_entities.entity_key`).all(requestedEntities, requestedMaterializations, this.projectId, sourceId);
12261
+ const rows = replacementAssertionRows(this.db, {
12262
+ projectId: this.projectId,
12263
+ sourceId,
12264
+ materializationId: materializationIds[0],
12265
+ incremental,
12266
+ includePrevious: materializationIds.length > 1,
12267
+ refs
12268
+ });
12045
12269
  const owned = new Set;
12046
12270
  const byMaterialization = new Map;
12047
12271
  for (const row of rows) {
12048
12272
  const source = storedSource(row);
12049
- const key = projectionEntityKey(source.assertion);
12273
+ const key = projectionEntityKey2(source.assertion);
12050
12274
  owned.add(key);
12051
12275
  const materialization = byMaterialization.get(row.materialization_id) ?? new Map;
12052
12276
  materialization.set(key, source);
@@ -12062,19 +12286,30 @@ class SqliteMaterializationStateReader {
12062
12286
  this.prepareReplacementLinks(input);
12063
12287
  }
12064
12288
  prepareReplacementObjects(input) {
12289
+ const rows = replacementSourceRows({
12290
+ projectId: this.projectId,
12291
+ sourceId: input.sourceId,
12292
+ materializationId: input.candidateMaterializationId,
12293
+ incremental: input.incremental ?? false
12294
+ });
12065
12295
  this.db.query(`
12066
12296
  INSERT INTO ${SQLITE_REPLACEMENT_WORK_TABLE} (
12067
12297
  session_id, entity_kind, identity_key, sort_key, diff_required
12068
12298
  )
12069
12299
  SELECT ?, 'object', json_array(object_type_id, primary_id),
12070
12300
  MIN(entity_sort_key), 1
12071
- FROM ontology_source_rows
12072
- WHERE project_id = ? AND source_id = ? AND entity_kind = 'object'
12073
- AND materialization_id IN (?, COALESCE(?, ''))
12301
+ FROM (${rows.sql}) AS rows
12302
+ WHERE entity_kind = 'object'
12074
12303
  GROUP BY object_type_id, primary_id
12075
- `).run(input.sessionId, this.projectId, input.sourceId, input.candidateMaterializationId, input.previousMaterializationId);
12304
+ `).run(input.sessionId, ...rows.values);
12076
12305
  }
12077
12306
  prepareReplacementLinks(input) {
12307
+ const rows = replacementSourceRows({
12308
+ projectId: this.projectId,
12309
+ sourceId: input.sourceId,
12310
+ materializationId: input.candidateMaterializationId,
12311
+ incremental: input.incremental ?? false
12312
+ });
12078
12313
  this.db.query(`
12079
12314
  WITH incident_objects AS (
12080
12315
  SELECT DISTINCT
@@ -12085,9 +12320,8 @@ class SqliteMaterializationStateReader {
12085
12320
  ), replacement_links AS (
12086
12321
  SELECT source_type_id, source_primary_id AS source_id, link_id,
12087
12322
  target_type_id, target_primary_id AS target_id
12088
- FROM ontology_source_rows
12089
- WHERE project_id = ? AND source_id = ? AND entity_kind = 'link'
12090
- AND materialization_id IN (?, COALESCE(?, ''))
12323
+ FROM (${rows.sql}) AS rows
12324
+ WHERE entity_kind = 'link'
12091
12325
  ), incident_links AS (
12092
12326
  SELECT links.source_type_id, links.source_id, links.link_id,
12093
12327
  links.target_type_id, links.target_id
@@ -12128,28 +12362,30 @@ class SqliteMaterializationStateReader {
12128
12362
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
12129
12363
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
12130
12364
  FROM ontology_source_rows AS rows
12131
- JOIN ontology_sources AS sources
12365
+ JOIN ontology_source_roots AS sources
12132
12366
  ON sources.project_id = rows.project_id
12133
12367
  AND sources.source_id = rows.source_id
12134
12368
  AND sources.materialization_id = rows.materialization_id
12369
+ AND sources.root_sort_key = rows.root_sort_key
12135
12370
  JOIN incident_objects
12136
12371
  ON incident_objects.object_type_id = rows.source_type_id
12137
12372
  AND incident_objects.primary_id = rows.source_primary_id
12138
12373
  WHERE rows.project_id = ? AND rows.entity_kind = 'link'
12139
- AND sources.status = 'active'
12374
+ AND sources.active = 1
12140
12375
  UNION
12141
12376
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
12142
12377
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
12143
12378
  FROM ontology_source_rows AS rows
12144
- JOIN ontology_sources AS sources
12379
+ JOIN ontology_source_roots AS sources
12145
12380
  ON sources.project_id = rows.project_id
12146
12381
  AND sources.source_id = rows.source_id
12147
12382
  AND sources.materialization_id = rows.materialization_id
12383
+ AND sources.root_sort_key = rows.root_sort_key
12148
12384
  JOIN incident_objects
12149
12385
  ON incident_objects.object_type_id = rows.target_type_id
12150
12386
  AND incident_objects.primary_id = rows.target_primary_id
12151
12387
  WHERE rows.project_id = ? AND rows.entity_kind = 'link'
12152
- AND sources.status = 'active'
12388
+ AND sources.active = 1
12153
12389
  ), diff_links AS (
12154
12390
  SELECT * FROM replacement_links
12155
12391
  UNION SELECT * FROM incident_links
@@ -12187,7 +12423,7 @@ class SqliteMaterializationStateReader {
12187
12423
  json_array(source_type_id, source_id, link_id, target_type_id, target_id),
12188
12424
  sort_key, diff_required
12189
12425
  FROM selected
12190
- `).run(input.sessionId, this.projectId, input.sourceId, input.candidateMaterializationId, input.previousMaterializationId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, input.sessionId);
12426
+ `).run(input.sessionId, ...rows.values, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, this.projectId, input.sessionId);
12191
12427
  }
12192
12428
  replacementRows(input, cursor) {
12193
12429
  return this.db.query(`
@@ -12225,9 +12461,9 @@ function activeSourceMap(rows) {
12225
12461
  const result = new Map;
12226
12462
  for (const row of rows) {
12227
12463
  const source = storedSource(row);
12228
- const key = projectionEntityKey(source.assertion);
12464
+ const key = projectionEntityKey2(source.assertion);
12229
12465
  if (result.has(key)) {
12230
- throw new MaterializationConflictError2("source-materialization", `Multiple active sources assert ${key}.`);
12466
+ throw new MaterializationConflictError3("source-materialization", `Multiple active sources assert ${key}.`);
12231
12467
  }
12232
12468
  result.set(key, source);
12233
12469
  }
@@ -12242,7 +12478,7 @@ function activeScopeSourceMap(rows) {
12242
12478
  const ref = source.assertion.ref;
12243
12479
  const key = linkScopeSortKey(ref.source, ref.linkId);
12244
12480
  if (result.has(key)) {
12245
- throw new MaterializationConflictError2("source-materialization", `Multiple active sources assert cardinality-one link slot ${key}.`);
12481
+ throw new MaterializationConflictError3("source-materialization", `Multiple active sources assert cardinality-one link slot ${key}.`);
12246
12482
  }
12247
12483
  result.set(key, source);
12248
12484
  }
@@ -12306,7 +12542,7 @@ function storedSource(row) {
12306
12542
  }
12307
12543
  function effectiveObjectSnapshot(row) {
12308
12544
  if (!row.last_commit_id) {
12309
- throw new MaterializationConflictError2("effective-state", `Effective object ${row.object_type_id}:${row.primary_id} lacks materializer provenance.`);
12545
+ throw new MaterializationConflictError3("effective-state", `Effective object ${row.object_type_id}:${row.primary_id} lacks materializer provenance.`);
12310
12546
  }
12311
12547
  return {
12312
12548
  ref: objectRefFromColumns(row),
@@ -12319,7 +12555,7 @@ function effectiveObjectSnapshot(row) {
12319
12555
  }
12320
12556
  function effectiveLinkSnapshot(row) {
12321
12557
  if (!row.last_commit_id) {
12322
- throw new MaterializationConflictError2("effective-state", `Effective link ${linkRefKey(linkRefFromColumns(row))} lacks materializer provenance.`);
12558
+ throw new MaterializationConflictError3("effective-state", `Effective link ${linkRefKey(linkRefFromColumns(row))} lacks materializer provenance.`);
12323
12559
  }
12324
12560
  return {
12325
12561
  ref: linkRefFromColumns(row),
@@ -12333,7 +12569,7 @@ function effectiveLinkSnapshot(row) {
12333
12569
  }
12334
12570
  function storedPoint(row) {
12335
12571
  if (!row.last_commit_id) {
12336
- throw new MaterializationConflictError2("timeseries-point", `Telemetry point ${telemetryPointSortKey({
12572
+ throw new MaterializationConflictError3("timeseries-point", `Telemetry point ${telemetryPointSortKey({
12337
12573
  object: { objectTypeId: row.object_type_id, primaryId: row.object_id },
12338
12574
  propertyId: row.property_id
12339
12575
  }, row.at)} lacks materializer provenance.`);
@@ -12372,7 +12608,7 @@ class SqliteMaterializationSessions {
12372
12608
  }
12373
12609
  create(header) {
12374
12610
  if (!this.context?.active) {
12375
- throw new MaterializationConflictError3("effective-state", "Materialization sessions require an active storage transaction.");
12611
+ throw new MaterializationConflictError4("effective-state", "Materialization sessions require an active storage transaction.");
12376
12612
  }
12377
12613
  this.ensureTables();
12378
12614
  const session = new SqliteMaterializationSessionState(structuredClone(header), this.context.id);
@@ -12384,7 +12620,7 @@ class SqliteMaterializationSessions {
12384
12620
  require(session) {
12385
12621
  const value = this.sessions.get(session.providerToken);
12386
12622
  if (!value || !value.active || !this.context?.active || value.transactionId !== this.context.id) {
12387
- throw new MaterializationConflictError3("effective-state", "Materialization session is inactive.");
12623
+ throw new MaterializationConflictError4("effective-state", "Materialization session is inactive.");
12388
12624
  }
12389
12625
  return value;
12390
12626
  }
@@ -12444,7 +12680,7 @@ class SqliteMaterializationSessions {
12444
12680
  const session = this.require(input.session);
12445
12681
  const stream = session.workStreams[input.order];
12446
12682
  if (stream.started) {
12447
- throw new MaterializationConflictError3("effective-state", `Materialization ${input.order} work may only be streamed once per session.`);
12683
+ throw new MaterializationConflictError4("effective-state", `Materialization ${input.order} work may only be streamed once per session.`);
12448
12684
  }
12449
12685
  stream.started = true;
12450
12686
  session.workSealed = true;
@@ -12721,7 +12957,7 @@ function recordSummary(summary, record, columns) {
12721
12957
  // src/ontology-storage/materialization-writer.ts
12722
12958
  import {
12723
12959
  linkRefKey as linkRefKey2,
12724
- MaterializationConflictError as MaterializationConflictError4,
12960
+ MaterializationConflictError as MaterializationConflictError5,
12725
12961
  objectRefKey as objectRefKey2,
12726
12962
  telemetryPointKey
12727
12963
  } from "@sixb/core/internal/materialization";
@@ -12893,7 +13129,7 @@ class SqliteMaterializationWriter {
12893
13129
  `).run(projectId, point.series.object.objectTypeId, point.series.object.primaryId, point.series.propertyId, canonicalJson(point.value), point.unit ?? null, point.at, point.lastCommitId);
12894
13130
  } catch (error) {
12895
13131
  if (isSqliteConstraintError(error)) {
12896
- throw new MaterializationConflictError4("timeseries-point", `Telemetry point ${telemetryPointKey(point.series, point.at)} changed.`);
13132
+ throw new MaterializationConflictError5("timeseries-point", `Telemetry point ${telemetryPointKey(point.series, point.at)} changed.`);
12897
13133
  }
12898
13134
  throw error;
12899
13135
  }
@@ -12955,13 +13191,13 @@ class SqliteOntologyMaterializationStorage {
12955
13191
  const linkScopeRevisions = reader.linkScopeRevisions(input.expected.linkScopes);
12956
13192
  for (const [index, expected] of input.expected.linkScopes.entries()) {
12957
13193
  if (linkScopeRevisions[index]?.fingerprint !== expected.fingerprint) {
12958
- throw new MaterializationConflictError5("effective-state", `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`);
13194
+ throw new MaterializationConflictError6("effective-state", `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`);
12959
13195
  }
12960
13196
  }
12961
13197
  for (const expected of input.expected.points) {
12962
13198
  const point = reader.exactPoint(expected.series, expected.at);
12963
13199
  if ((point?.lastCommitId ?? null) !== expected.lastCommitId) {
12964
- throw new MaterializationConflictError5("timeseries-point", `Telemetry point ${telemetryPointKey2(expected.series, expected.at)} changed.`);
13200
+ throw new MaterializationConflictError6("timeseries-point", `Telemetry point ${telemetryPointKey2(expected.series, expected.at)} changed.`);
12965
13201
  }
12966
13202
  }
12967
13203
  return session.publicSession();
@@ -13019,10 +13255,10 @@ class SqliteOntologyMaterializationStorage {
13019
13255
  const reader = new SqliteMaterializationStateReader(this.db, session.header.commit.projectId);
13020
13256
  if (input.entityKind === "object") {
13021
13257
  if (replacement.projectionKind !== "object") {
13022
- throw new MaterializationConflictError5("source-materialization", "Link projection replacement cannot stream object state.");
13258
+ throw new MaterializationConflictError6("source-materialization", "Link projection replacement cannot stream object state.");
13023
13259
  }
13024
13260
  if (replacement.objectStreamStarted) {
13025
- throw new MaterializationConflictError5("effective-state", "Replacement object state may only be streamed once per session.");
13261
+ throw new MaterializationConflictError6("effective-state", "Replacement object state may only be streamed once per session.");
13026
13262
  }
13027
13263
  replacement.objectStreamStarted = true;
13028
13264
  for (const identities of reader.replacementIdentities({
@@ -13030,6 +13266,7 @@ class SqliteOntologyMaterializationStorage {
13030
13266
  sourceId: replacement.sourceId,
13031
13267
  candidateMaterializationId: replacement.candidateMaterializationId,
13032
13268
  previousMaterializationId: replacement.previousMaterializationId,
13269
+ incremental: replacement.incremental,
13033
13270
  kind: "object",
13034
13271
  pageRows: input.pageRows
13035
13272
  })) {
@@ -13048,10 +13285,10 @@ class SqliteOntologyMaterializationStorage {
13048
13285
  return;
13049
13286
  }
13050
13287
  if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
13051
- throw new MaterializationConflictError5("effective-state", "Object projection replacement must fully stream object state before link state.");
13288
+ throw new MaterializationConflictError6("effective-state", "Object projection replacement must fully stream object state before link state.");
13052
13289
  }
13053
13290
  if (replacement.linkStreamStarted) {
13054
- throw new MaterializationConflictError5("effective-state", "Replacement link state may only be streamed once per session.");
13291
+ throw new MaterializationConflictError6("effective-state", "Replacement link state may only be streamed once per session.");
13055
13292
  }
13056
13293
  replacement.linkStreamStarted = true;
13057
13294
  const materializationIds = [
@@ -13063,6 +13300,7 @@ class SqliteOntologyMaterializationStorage {
13063
13300
  sourceId: replacement.sourceId,
13064
13301
  candidateMaterializationId: replacement.candidateMaterializationId,
13065
13302
  previousMaterializationId: replacement.previousMaterializationId,
13303
+ incremental: replacement.incremental,
13066
13304
  kind: "link",
13067
13305
  pageRows: input.pageRows
13068
13306
  })) {
@@ -13073,7 +13311,7 @@ class SqliteOntologyMaterializationStorage {
13073
13311
  }
13074
13312
  return identity;
13075
13313
  });
13076
- const links = reader.replacementLinkStates(replacement.sourceId, replacement.candidateMaterializationId, materializationIds, linkIdentities);
13314
+ const links = reader.replacementLinkStates(replacement.sourceId, replacement.candidateMaterializationId, materializationIds, linkIdentities, replacement.incremental);
13077
13315
  yield { objects: [], links };
13078
13316
  }
13079
13317
  this.sessions.require(input.session);
@@ -13125,14 +13363,14 @@ class SqliteOntologyMaterializationStorage {
13125
13363
  requireReplacement(session, input) {
13126
13364
  if (session.replacement) {
13127
13365
  if (session.replacement.sourceId !== input.source.projectionId || session.replacement.candidateMaterializationId !== input.candidateMaterializationId) {
13128
- throw new MaterializationConflictError5("source-materialization", "Materialization session already owns another replacement union.");
13366
+ throw new MaterializationConflictError6("source-materialization", "Materialization session already owns another replacement union.");
13129
13367
  }
13130
13368
  return session.replacement;
13131
13369
  }
13132
13370
  const projectId = session.header.commit.projectId;
13133
13371
  const candidate = this.getSource(projectId, input.source.projectionId, input.candidateMaterializationId);
13134
13372
  if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
13135
- throw new MaterializationConflictError5("source-materialization", `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`);
13373
+ throw new MaterializationConflictError6("source-materialization", `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`);
13136
13374
  }
13137
13375
  assertProjectionExecution(this.db, {
13138
13376
  projectId,
@@ -13142,12 +13380,13 @@ class SqliteOntologyMaterializationStorage {
13142
13380
  });
13143
13381
  const previous = this.getActiveSource(projectId, input.source.projectionId);
13144
13382
  if (candidate.protocol !== "replacement" || previous && (previous.protocol !== candidate.protocol || previous.projection_kind !== candidate.projection_kind)) {
13145
- throw new MaterializationConflictError5("source-materialization", "Source replacement kind or protocol does not match its active materialization.");
13383
+ throw new MaterializationConflictError6("source-materialization", "Source replacement kind or protocol does not match its active materialization.");
13146
13384
  }
13147
13385
  session.replacement = {
13148
13386
  sourceId: input.source.projectionId,
13149
13387
  candidateMaterializationId: input.candidateMaterializationId,
13150
13388
  previousMaterializationId: previous?.materialization_id ?? null,
13389
+ incremental: candidate.base_materialization_id !== null,
13151
13390
  projectionKind: candidate.projection_kind,
13152
13391
  objectStreamStarted: false,
13153
13392
  objectStreamCompleted: false,
@@ -13176,38 +13415,38 @@ class SqliteOntologyMaterializationStorage {
13176
13415
  if (!duplicate)
13177
13416
  return;
13178
13417
  if (duplicate.duplicate === "origin") {
13179
- throw new MaterializationConflictError5("run-correlation", "Ontology commit origin already has an authoritative commit.");
13418
+ throw new MaterializationConflictError6("run-correlation", "Ontology commit origin already has an authoritative commit.");
13180
13419
  }
13181
- throw new MaterializationConflictError5("idempotency", duplicate.duplicate === "id" ? `Ontology commit '${header.commit.id}' already exists.` : "Ontology idempotency key already exists.");
13420
+ throw new MaterializationConflictError6("idempotency", duplicate.duplicate === "id" ? `Ontology commit '${header.commit.id}' already exists.` : "Ontology idempotency key already exists.");
13182
13421
  }
13183
13422
  assertSource(expected, projectId) {
13184
13423
  const active = this.getActiveSource(projectId, expected.source.projectionId);
13185
13424
  if ((active?.materialization_id ?? null) !== expected.activeMaterializationId || (active?.last_commit_id ?? null) !== expected.lastCommitId) {
13186
- throw new MaterializationConflictError5("projection-fence", `Source '${expected.source.projectionId}' changed.`);
13425
+ throw new MaterializationConflictError6("projection-fence", `Source '${expected.source.projectionId}' changed.`);
13187
13426
  }
13188
13427
  }
13189
13428
  assertObject(reader, expected) {
13190
13429
  const row = reader.effectiveObjectRevision(expected.ref);
13191
13430
  if (!expected.exists) {
13192
13431
  if (row) {
13193
- throw new MaterializationConflictError5("effective-state", `Expected object ${objectRefKey3(expected.ref)} to be absent.`);
13432
+ throw new MaterializationConflictError6("effective-state", `Expected object ${objectRefKey3(expected.ref)} to be absent.`);
13194
13433
  }
13195
13434
  return;
13196
13435
  }
13197
13436
  if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
13198
- throw new MaterializationConflictError5("effective-state", `Expected object ${objectRefKey3(expected.ref)} changed.`);
13437
+ throw new MaterializationConflictError6("effective-state", `Expected object ${objectRefKey3(expected.ref)} changed.`);
13199
13438
  }
13200
13439
  }
13201
13440
  assertLink(reader, expected) {
13202
13441
  const lastCommitId = reader.effectiveLinkLastCommit(expected.ref);
13203
13442
  if (!expected.exists) {
13204
13443
  if (lastCommitId !== undefined) {
13205
- throw new MaterializationConflictError5("effective-state", `Expected link ${linkRefKey3(expected.ref)} to be absent.`);
13444
+ throw new MaterializationConflictError6("effective-state", `Expected link ${linkRefKey3(expected.ref)} to be absent.`);
13206
13445
  }
13207
13446
  return;
13208
13447
  }
13209
13448
  if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
13210
- throw new MaterializationConflictError5("effective-state", `Expected link ${linkRefKey3(expected.ref)} changed.`);
13449
+ throw new MaterializationConflictError6("effective-state", `Expected link ${linkRefKey3(expected.ref)} changed.`);
13211
13450
  }
13212
13451
  }
13213
13452
  async assertFinalization(session, input) {
@@ -13301,7 +13540,7 @@ class SqliteOntologyMaterializationStorage {
13301
13540
  assertSourceActivationCorrelation(session, activation);
13302
13541
  const candidate = this.getSource(commit.projectId, activation.source.projectionId, activation.materializationId);
13303
13542
  if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
13304
- throw new MaterializationConflictError5("source-materialization", "Source activation candidate is missing or is not ready.");
13543
+ throw new MaterializationConflictError6("source-materialization", "Source activation candidate is missing or is not ready.");
13305
13544
  }
13306
13545
  assertProjectionExecution(this.db, {
13307
13546
  projectId: commit.projectId,
@@ -13313,6 +13552,7 @@ class SqliteOntologyMaterializationStorage {
13313
13552
  invalidCorrelation2("Source activation does not match its ready candidate identity.");
13314
13553
  }
13315
13554
  this.assertSource(activation.expected, commit.projectId);
13555
+ activateSourceRoots(this.db, commit.projectId, candidate, activation);
13316
13556
  const previous = this.getActiveSource(commit.projectId, activation.source.projectionId);
13317
13557
  if (previous) {
13318
13558
  assertPinnedDatasetWatermark({
@@ -13352,7 +13592,7 @@ class SqliteOntologyMaterializationStorage {
13352
13592
  `).run(commit.projectId, commit.id, commit.idempotencyKey, commit.requestHash, commit.executionId, origin.kind, origin.runId, origin.batchOrdinal, canonicalJson(commit.origin), commit.actor === undefined ? null : canonicalJson(commit.actor), commit.ontologyRevision, commit.projectionRevision ?? null, commit.ownershipHash ?? null, canonicalJson(commit.intent), canonicalJson(input.finalization.result), commit.committedAt);
13353
13593
  } catch (error) {
13354
13594
  if (isSqliteConstraintError(error)) {
13355
- throw new MaterializationConflictError5("idempotency", "Ontology commit identity already exists.");
13595
+ throw new MaterializationConflictError6("idempotency", "Ontology commit identity already exists.");
13356
13596
  }
13357
13597
  throw error;
13358
13598
  }
@@ -13372,8 +13612,8 @@ class SqliteOntologyMaterializationStorage {
13372
13612
  // src/ontology-storage/outbox.ts
13373
13613
  import { serializeSixbFailure as serializeSixbFailure2 } from "@sixb/core/internal/errors";
13374
13614
  import {
13375
- MaterializationConflictError as MaterializationConflictError6,
13376
- MaterializationValidationError
13615
+ MaterializationConflictError as MaterializationConflictError7,
13616
+ MaterializationValidationError as MaterializationValidationError2
13377
13617
  } from "@sixb/core/internal/materialization";
13378
13618
  import { ONTOLOGY_OUTBOX_FAILURE_CODES as ONTOLOGY_OUTBOX_FAILURE_CODES2 } from "@sixb/core/storage";
13379
13619
  class SqliteOntologyOutboxStorage {
@@ -13391,25 +13631,31 @@ class SqliteOntologyOutboxStorage {
13391
13631
  const leaseExpiry = assertTimestamp(input.leaseExpiresAt, "Ontology outbox lease expiry");
13392
13632
  assertNonblank(input.leaseId, "Ontology outbox lease id");
13393
13633
  if (leaseExpiry <= now) {
13394
- throw new MaterializationValidationError("Ontology outbox lease expiry must be later than the claim time.");
13634
+ throw new MaterializationValidationError2("Ontology outbox lease expiry must be later than the claim time.");
13395
13635
  }
13636
+ const eligible = this.db.query(`SELECT CAST(rowid AS TEXT) AS rowId
13637
+ FROM ontology_outbox INDEXED BY idx_ontology_outbox_claim
13638
+ WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
13639
+ AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
13640
+ LIMIT ?`).all(input.projectId, input.now, input.now, input.limit + 1);
13641
+ if (eligible.length === 0)
13642
+ return [];
13643
+ const selected = eligible.length <= input.limit ? eligible : this.db.query(`SELECT CAST(rowid AS TEXT) AS rowId
13644
+ FROM ontology_outbox INDEXED BY idx_ontology_outbox_publication_order
13645
+ WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
13646
+ AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
13647
+ ORDER BY created_at, commit_id, commit_ordinal
13648
+ LIMIT ?`).all(input.projectId, input.now, input.now, input.limit);
13396
13649
  const rows = this.db.query(`
13397
13650
  UPDATE ontology_outbox
13398
13651
  SET attempts = attempts + 1, lease_id = ?, lease_expires_at = ?
13399
- WHERE rowid IN (
13400
- SELECT rowid
13401
- FROM ontology_outbox
13402
- WHERE project_id = ? AND published_at IS NULL AND available_at <= ?
13403
- AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
13404
- ORDER BY created_at, commit_id, commit_ordinal
13405
- LIMIT ?
13406
- )
13652
+ WHERE rowid IN (SELECT CAST(value AS INTEGER) FROM json_each(?))
13407
13653
  RETURNING envelope, available_at, attempts, lease_id, lease_expires_at,
13408
13654
  published_at, last_failure, created_at
13409
- `).all(input.leaseId, input.leaseExpiresAt, input.projectId, input.now, input.now, input.limit);
13655
+ `).all(input.leaseId, input.leaseExpiresAt, JSON.stringify(selected.map((row) => row.rowId)));
13410
13656
  return rows.map((row) => outboxRecord(row)).sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.envelope.id.localeCompare(right.envelope.id)).map((record) => {
13411
13657
  if (record.leaseId === null || record.leaseExpiresAt === null) {
13412
- throw new MaterializationConflictError6("outbox-lease", "Ontology outbox claim returned an unpaired lease.");
13658
+ throw new MaterializationConflictError7("outbox-lease", "Ontology outbox claim returned an unpaired lease.");
13413
13659
  }
13414
13660
  return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt };
13415
13661
  });
@@ -13424,8 +13670,9 @@ class SqliteOntologyOutboxStorage {
13424
13670
  WITH requested(id) AS MATERIALIZED (
13425
13671
  SELECT CAST(value AS TEXT) FROM json_each(?)
13426
13672
  ), leased(id) AS MATERIALIZED (
13427
- SELECT outbox.id FROM ontology_outbox AS outbox
13428
- JOIN requested USING (id)
13673
+ -- Drive validation from the bounded ID list even before ANALYZE.
13674
+ SELECT outbox.id FROM requested
13675
+ CROSS JOIN ontology_outbox AS outbox USING (id)
13429
13676
  WHERE outbox.project_id = ? AND outbox.lease_id = ?
13430
13677
  AND outbox.lease_expires_at IS NOT NULL
13431
13678
  ), eligible(id) AS (
@@ -13450,8 +13697,9 @@ class SqliteOntologyOutboxStorage {
13450
13697
  WITH requested(id) AS MATERIALIZED (
13451
13698
  SELECT CAST(value AS TEXT) FROM json_each(?)
13452
13699
  ), leased(id) AS MATERIALIZED (
13453
- SELECT outbox.id FROM ontology_outbox AS outbox
13454
- JOIN requested USING (id)
13700
+ -- Drive validation from the bounded ID list even before ANALYZE.
13701
+ SELECT outbox.id FROM requested
13702
+ CROSS JOIN ontology_outbox AS outbox USING (id)
13455
13703
  WHERE outbox.project_id = ? AND outbox.lease_id = ?
13456
13704
  AND outbox.lease_expires_at IS NOT NULL
13457
13705
  ), eligible(id) AS (
@@ -13508,7 +13756,7 @@ function validateLeaseIds(ids) {
13508
13756
  for (const id of ids) {
13509
13757
  assertNonblank(id, "Ontology outbox event id");
13510
13758
  if (seen.has(id)) {
13511
- throw new MaterializationValidationError(`Ontology outbox lease batch repeats event '${id}'.`);
13759
+ throw new MaterializationValidationError2(`Ontology outbox lease batch repeats event '${id}'.`);
13512
13760
  }
13513
13761
  seen.add(id);
13514
13762
  }
@@ -13519,11 +13767,11 @@ function assertLeaseInput(input) {
13519
13767
  assertNonblank(input.leaseId, "Ontology outbox lease id");
13520
13768
  }
13521
13769
  function leaseConflict() {
13522
- return new MaterializationConflictError6("outbox-lease", "Ontology outbox lease does not match.");
13770
+ return new MaterializationConflictError7("outbox-lease", "Ontology outbox lease does not match.");
13523
13771
  }
13524
13772
 
13525
13773
  // src/ontology-storage/sources.ts
13526
- import { MaterializationValidationError as MaterializationValidationError2 } from "@sixb/core/internal/materialization";
13774
+ import { MaterializationValidationError as MaterializationValidationError3 } from "@sixb/core/internal/materialization";
13527
13775
  import {
13528
13776
  assertSourceBeginInput as assertBeginInput,
13529
13777
  assertSourceExecutionIdentity as assertExecutionIdentity,
@@ -13537,8 +13785,53 @@ import {
13537
13785
  sourceMaterializationIdentity,
13538
13786
  sourceStageRow,
13539
13787
  sourceStageRows,
13540
- utf8SortKey
13788
+ utf8SortKey as utf8SortKey2
13541
13789
  } from "@sixb/core/internal/ontology-storage-provider";
13790
+
13791
+ // src/ontology-storage/source-cleanup.ts
13792
+ function cleanupSourceVersions(db, input) {
13793
+ let remaining = input.limit;
13794
+ let rowsDeleted = db.query(`DELETE FROM ontology_source_rows WHERE rowid IN (
13795
+ SELECT rows.rowid FROM ontology_source_roots AS roots
13796
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
13797
+ CROSS JOIN ontology_source_rows AS rows
13798
+ ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
13799
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
13800
+ WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
13801
+ AND sources.status IN ('superseded', 'abandoned')
13802
+ ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
13803
+ LIMIT ?
13804
+ )`).run(input.projectId, input.terminalBefore, remaining).changes;
13805
+ remaining -= rowsDeleted;
13806
+ if (remaining > 0) {
13807
+ const removed = db.query(`DELETE FROM ontology_source_roots WHERE rowid IN (
13808
+ SELECT roots.rowid FROM ontology_source_roots AS roots
13809
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
13810
+ WHERE roots.project_id = ? AND roots.active = 0 AND roots.retired_at < ?
13811
+ AND sources.status IN ('superseded', 'abandoned')
13812
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
13813
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
13814
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
13815
+ ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key LIMIT ?
13816
+ )`).run(input.projectId, input.terminalBefore, remaining).changes;
13817
+ rowsDeleted += removed;
13818
+ remaining -= removed;
13819
+ }
13820
+ const materializationsDeleted = remaining === 0 ? 0 : db.query(`DELETE FROM ontology_sources WHERE rowid IN (
13821
+ SELECT sources.rowid FROM ontology_sources AS sources
13822
+ WHERE project_id = ? AND status IN ('superseded', 'abandoned') AND terminal_at < ?
13823
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
13824
+ WHERE roots.project_id = sources.project_id AND roots.source_id = sources.source_id
13825
+ AND roots.materialization_id = sources.materialization_id)
13826
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
13827
+ WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
13828
+ AND rows.materialization_id = sources.materialization_id)
13829
+ ORDER BY terminal_at, source_id, materialization_id LIMIT ?
13830
+ )`).run(input.projectId, input.terminalBefore, remaining).changes;
13831
+ return { rowsDeleted, materializationsDeleted };
13832
+ }
13833
+
13834
+ // src/ontology-storage/sources.ts
13542
13835
  class SqliteOntologySourceStorage {
13543
13836
  db;
13544
13837
  runRootOperation;
@@ -13573,10 +13866,10 @@ class SqliteOntologySourceStorage {
13573
13866
  dataset_id, dataset_version_id, dataset_version_created_at,
13574
13867
  projection_revision, ownership_hash, ontology_revision,
13575
13868
  root_count, assertion_count, created_at, ready_at, activated_at,
13576
- terminal_at, last_commit_id, updated_at
13869
+ terminal_at, last_commit_id, updated_at, base_materialization_id, base_commit_id
13577
13870
  ) VALUES (?, ?, ?, ?, ?, 'replacement', 'staging', ?, ?, ?, ?, ?, ?, ?,
13578
- NULL, NULL, ?, NULL, NULL, NULL, NULL, ?)
13579
- `).run(input.projectId, input.source.projectionId, input.materializationId, input.execution.projectionRunId, input.projectionKind, input.execution.executionToken, input.datasetVersion.datasetId, input.datasetVersion.versionId, input.datasetVersion.createdAt, input.projectionRevision, input.ownershipHash, input.ontologyRevision, input.createdAt, input.createdAt);
13871
+ NULL, NULL, ?, NULL, NULL, NULL, NULL, ?, ?, ?)
13872
+ `).run(input.projectId, input.source.projectionId, input.materializationId, input.execution.projectionRunId, input.projectionKind, input.execution.executionToken, input.datasetVersion.datasetId, input.datasetVersion.versionId, input.datasetVersion.createdAt, input.projectionRevision, input.ownershipHash, input.ontologyRevision, input.createdAt, input.createdAt, input.base?.materializationId ?? null, input.base?.lastCommitId ?? null);
13580
13873
  return sourceRecord(this.requireManifest(input.projectId, input.source.projectionId, input.materializationId));
13581
13874
  });
13582
13875
  }
@@ -13592,6 +13885,7 @@ class SqliteOntologySourceStorage {
13592
13885
  const rows = sourceStageRows(manifest.projection_kind, input.rows);
13593
13886
  const existing = this.findStageRows(input, rows);
13594
13887
  const { pending, unchanged } = reconcileSourceStageRows(rows, existing);
13888
+ stageSourceRoots(this.db, manifest, input);
13595
13889
  this.insertStageRows(input, pending);
13596
13890
  return { inserted: pending.length, unchanged };
13597
13891
  });
@@ -13617,7 +13911,7 @@ class SqliteOntologySourceStorage {
13617
13911
  throw sourceConflict(`Source materialization '${input.materializationId}' cannot transition from '${manifest.status}' to 'ready'.`);
13618
13912
  }
13619
13913
  if (input.readyAt < manifest.created_at) {
13620
- throw new MaterializationValidationError2("Source readyAt cannot precede source createdAt.");
13914
+ throw new MaterializationValidationError3("Source readyAt cannot precede source createdAt.");
13621
13915
  }
13622
13916
  this.assertReadyState(manifest, input.rootCount, input.assertionCount);
13623
13917
  const result = this.db.query(`
@@ -13656,55 +13950,7 @@ class SqliteOntologySourceStorage {
13656
13950
  assertNonblank(input.projectId, "Terminal source cleanup project id");
13657
13951
  assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true);
13658
13952
  assertPositiveInteger(input.limit, "Terminal source cleanup limit");
13659
- const manifests = this.db.query(`
13660
- SELECT * FROM ontology_sources
13661
- WHERE project_id = ? AND status IN ('superseded', 'abandoned')
13662
- AND terminal_at < ?
13663
- ORDER BY terminal_at, source_id, materialization_id
13664
- LIMIT ?
13665
- `).all(input.projectId, input.terminalBefore, input.limit);
13666
- let remaining = input.limit;
13667
- let rowsDeleted = 0;
13668
- let materializationsDeleted = 0;
13669
- for (const manifest of manifests) {
13670
- if (remaining === 0)
13671
- break;
13672
- const deleted = this.db.query(`
13673
- DELETE FROM ontology_source_rows
13674
- WHERE rowid IN (
13675
- SELECT rows.rowid
13676
- FROM ontology_source_rows AS rows
13677
- JOIN ontology_sources AS sources
13678
- ON sources.project_id = rows.project_id
13679
- AND sources.source_id = rows.source_id
13680
- AND sources.materialization_id = rows.materialization_id
13681
- WHERE rows.project_id = ? AND rows.source_id = ? AND rows.materialization_id = ?
13682
- AND sources.status IN ('superseded', 'abandoned')
13683
- AND sources.terminal_at = ? AND sources.terminal_at < ?
13684
- ORDER BY rows.entity_sort_key
13685
- LIMIT ?
13686
- )
13687
- `).run(manifest.project_id, manifest.source_id, manifest.materialization_id, manifest.terminal_at, input.terminalBefore, remaining).changes;
13688
- rowsDeleted += deleted;
13689
- remaining -= deleted;
13690
- if (remaining === 0)
13691
- break;
13692
- const removed = this.db.query(`
13693
- DELETE FROM ontology_sources
13694
- WHERE project_id = ? AND source_id = ? AND materialization_id = ?
13695
- AND status IN ('superseded', 'abandoned')
13696
- AND terminal_at = ? AND terminal_at < ?
13697
- AND NOT EXISTS (
13698
- SELECT 1 FROM ontology_source_rows AS rows
13699
- WHERE rows.project_id = ontology_sources.project_id
13700
- AND rows.source_id = ontology_sources.source_id
13701
- AND rows.materialization_id = ontology_sources.materialization_id
13702
- )
13703
- `).run(manifest.project_id, manifest.source_id, manifest.materialization_id, manifest.terminal_at, input.terminalBefore).changes;
13704
- materializationsDeleted += removed;
13705
- remaining -= removed;
13706
- }
13707
- return { rowsDeleted, materializationsDeleted };
13953
+ return cleanupSourceVersions(this.db, input);
13708
13954
  });
13709
13955
  }
13710
13956
  async summarizeTerminal(input) {
@@ -13748,7 +13994,7 @@ class SqliteOntologySourceStorage {
13748
13994
  }
13749
13995
  transitionToAbandoned(manifest, abandonedAt) {
13750
13996
  if (abandonedAt < manifest.created_at || manifest.ready_at !== null && abandonedAt < manifest.ready_at) {
13751
- throw new MaterializationValidationError2("Source abandonedAt cannot precede source creation or readiness.");
13997
+ throw new MaterializationValidationError3("Source abandonedAt cannot precede source creation or readiness.");
13752
13998
  }
13753
13999
  const changed = this.db.query(`
13754
14000
  UPDATE ontology_sources
@@ -13759,19 +14005,23 @@ class SqliteOntologySourceStorage {
13759
14005
  if (changed !== 1) {
13760
14006
  throw sourceConflict(`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`);
13761
14007
  }
14008
+ this.db.query(`UPDATE ontology_source_roots SET retired_at = ?
14009
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ? AND active = 0`).run(abandonedAt, manifest.project_id, manifest.source_id, manifest.materialization_id);
13762
14010
  return sourceRecord(this.requireManifest(manifest.project_id, manifest.source_id, manifest.materialization_id));
13763
14011
  }
13764
14012
  assertReadyState(manifest, rootCount, assertionCount) {
13765
14013
  const counts = this.db.query(`
13766
- SELECT COUNT(*) AS assertions, COUNT(DISTINCT root_key) AS roots,
14014
+ SELECT (SELECT COUNT(*) FROM ontology_source_rows
14015
+ WHERE project_id = ? AND source_id = ? AND materialization_id = ?) AS assertions, COUNT(*) AS roots,
13767
14016
  COUNT(DISTINCT staging_ordinal) AS ordinals,
13768
14017
  MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
13769
- FROM ontology_source_rows
14018
+ FROM ontology_source_roots
13770
14019
  WHERE project_id = ? AND source_id = ? AND materialization_id = ?
13771
- `).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
14020
+ `).get(manifest.project_id, manifest.source_id, manifest.materialization_id, manifest.project_id, manifest.source_id, manifest.materialization_id);
13772
14021
  if (counts.assertions !== assertionCount || counts.roots !== rootCount || counts.ordinals !== rootCount || rootCount > 0 && (counts.min_ordinal !== 0 || counts.max_ordinal !== rootCount - 1)) {
13773
- throw new MaterializationValidationError2("Source ready counts do not match the staged roots and assertions.");
14022
+ throw new MaterializationValidationError3("Source ready counts do not match the staged roots and assertions.");
13774
14023
  }
14024
+ assertSourceRootCoverage(this.db, manifest);
13775
14025
  const invalid = this.db.query(manifest.projection_kind === "link" ? `
13776
14026
  SELECT root_key
13777
14027
  FROM ontology_source_rows
@@ -13797,15 +14047,15 @@ class SqliteOntologySourceStorage {
13797
14047
  LIMIT 1
13798
14048
  `).get(manifest.project_id, manifest.source_id, manifest.materialization_id);
13799
14049
  if (invalid) {
13800
- throw new MaterializationValidationError2(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.");
14050
+ throw new MaterializationValidationError3(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.");
13801
14051
  }
13802
14052
  }
13803
14053
  findStageRows(input, rows) {
13804
14054
  if (rows.length === 0)
13805
14055
  return [];
13806
- const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey(row.rootKey)))];
14056
+ const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.rootKey)))];
13807
14057
  const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))];
13808
- const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey(row.entityKey)))];
14058
+ const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.entityKey)))];
13809
14059
  const existing = this.db.query(`
13810
14060
  SELECT * FROM ontology_source_rows
13811
14061
  WHERE project_id = ? AND source_id = ? AND materialization_id = ?
@@ -13826,10 +14076,10 @@ class SqliteOntologySourceStorage {
13826
14076
  return {
13827
14077
  entityKind: row.assertion.kind,
13828
14078
  entityKey,
13829
- entitySortKey: utf8SortKey(entityKey),
14079
+ entitySortKey: utf8SortKey2(entityKey),
13830
14080
  rootKind: row.root.kind,
13831
14081
  rootKey,
13832
- rootSortKey: utf8SortKey(rootKey),
14082
+ rootSortKey: utf8SortKey2(rootKey),
13833
14083
  stagingOrdinal: row.stagingOrdinal,
13834
14084
  root: row.root,
13835
14085
  assertion: row.assertion,
@@ -14341,7 +14591,7 @@ class SqliteProjectionRunStorage {
14341
14591
  input_exhausted = ?, missing_target_object_type_id = NULL,
14342
14592
  missing_target_object_id = NULL, missing_target_batch_ordinal = NULL,
14343
14593
  missing_target_first_seen_at = NULL, source_rows_read = 0,
14344
- source_rows_skipped = 0, error = NULL
14594
+ source_rows_skipped = 0, source_changes_read = NULL, error = NULL
14345
14595
  WHERE project_id = ? AND id = ? AND status = 'failed'
14346
14596
  AND json_extract(error, '$.code') = 'queue.enqueue_failed'
14347
14597
  `).run(queuedAt.toISOString(), input.fixedBatchSize === undefined ? null : 0, input.fixedBatchSize === undefined ? null : 0, input.fixedBatchSize === undefined ? null : 0, input.projectId, input.id);
@@ -14436,10 +14686,11 @@ class SqliteProjectionRunStorage {
14436
14686
  execution_token = NULL,
14437
14687
  source_rows_read = ?,
14438
14688
  source_rows_skipped = ?,
14689
+ source_changes_read = ?,
14439
14690
  input_exhausted = ?,
14440
14691
  error = ?
14441
14692
  WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
14442
- `).run(input.status, plan.finishedAt.toISOString(), plan.progress.sourceRowsRead, plan.progress.sourceRowsSkipped, plan.inputExhausted ? 1 : existingRow.input_exhausted, plan.error === undefined ? null : serializeSixbFailure4(plan.error, PROJECTION_RUN_FAILURE_CODES), input.projectId, input.id, input.executionToken);
14693
+ `).run(input.status, plan.finishedAt.toISOString(), plan.progress.sourceRowsRead, plan.progress.sourceRowsSkipped, plan.progress.sourceChangesRead ?? null, plan.inputExhausted ? 1 : existingRow.input_exhausted, plan.error === undefined ? null : serializeSixbFailure4(plan.error, PROJECTION_RUN_FAILURE_CODES), input.projectId, input.id, input.executionToken);
14443
14694
  if (result.changes !== 1)
14444
14695
  throw staleProjectionRunExecution(input.id);
14445
14696
  return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id));
@@ -14605,6 +14856,7 @@ class SqliteProjectionRunStorage {
14605
14856
  const args = [
14606
14857
  progress.sourceRowsRead,
14607
14858
  progress.sourceRowsSkipped,
14859
+ progress.sourceChangesRead ?? null,
14608
14860
  existing.project_id,
14609
14861
  existing.id,
14610
14862
  executionToken
@@ -14613,7 +14865,8 @@ class SqliteProjectionRunStorage {
14613
14865
  UPDATE projection_runs
14614
14866
  SET
14615
14867
  source_rows_read = ?,
14616
- source_rows_skipped = ?
14868
+ source_rows_skipped = ?,
14869
+ source_changes_read = ?
14617
14870
  WHERE project_id = ? AND id = ? AND status = 'running'
14618
14871
  AND execution_token = ?
14619
14872
  `).run(...args);
@@ -14683,7 +14936,8 @@ function restoreProjectionRunRow(row) {
14683
14936
  missingTargetFirstSeenAt: row.missing_target_first_seen_at ? new Date(row.missing_target_first_seen_at) : undefined,
14684
14937
  progress: {
14685
14938
  sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
14686
- sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped")
14939
+ sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
14940
+ ...row.source_changes_read == null ? {} : { sourceChangesRead: databaseSafeInteger(row.source_changes_read, "sourceChangesRead") }
14687
14941
  },
14688
14942
  error: row.error ?? undefined
14689
14943
  };
@@ -16758,4 +17012,4 @@ export {
16758
17012
  migrateSqliteStorage
16759
17013
  };
16760
17014
 
16761
- //# debugId=8848EEC97F2007FB64756E2164756E21
17015
+ //# debugId=6BCAC863183D761764756E2164756E21