@sixb/pg 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 (30) hide show
  1. package/dist/index.js +481 -227
  2. package/dist/index.js.map +13 -11
  3. package/dist/migrations.d.ts.map +1 -1
  4. package/dist/ontology-storage/materialization-session.d.ts +3 -0
  5. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  6. package/dist/ontology-storage/materialization-state.d.ts +2 -1
  7. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  8. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  9. package/dist/ontology-storage/shared.d.ts +2 -0
  10. package/dist/ontology-storage/shared.d.ts.map +1 -1
  11. package/dist/ontology-storage/source-cleanup.d.ts +4 -0
  12. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
  13. package/dist/ontology-storage/source-roots.d.ts +24 -0
  14. package/dist/ontology-storage/source-roots.d.ts.map +1 -0
  15. package/dist/ontology-storage/sources.d.ts.map +1 -1
  16. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  17. package/package.json +3 -3
  18. package/src/connector-connection-storage.ts +13 -3
  19. package/src/migrations/038-outbox-publication-order.sql +5 -0
  20. package/src/migrations/039-projection-source-roots.sql +46 -0
  21. package/src/migrations/040-connector-optional-pkce.sql +3 -0
  22. package/src/migrations.ts +13 -0
  23. package/src/ontology-storage/materialization-session.ts +24 -8
  24. package/src/ontology-storage/materialization-state.ts +65 -86
  25. package/src/ontology-storage/materializations.ts +39 -13
  26. package/src/ontology-storage/shared.ts +10 -0
  27. package/src/ontology-storage/source-cleanup.ts +61 -0
  28. package/src/ontology-storage/source-roots.ts +251 -0
  29. package/src/ontology-storage/sources.ts +21 -57
  30. package/src/pg-projection-run-storage.ts +8 -2
package/dist/index.js CHANGED
@@ -3448,7 +3448,7 @@ class PgConnectorConnectionPersistence {
3448
3448
  ) VALUES (
3449
3449
  ${record.projectId}, ${record.connectorId}, ${record.id}, ${record.slot},
3450
3450
  ${record.initiatedByExecutionId}, ${record.stateHash},
3451
- ${jsonParameter(this.sql, record.codeVerifier)}, ${record.redirectUri},
3451
+ ${record.codeVerifier === undefined ? null : jsonParameter(this.sql, record.codeVerifier)}, ${record.redirectUri},
3452
3452
  ${record.connectionRunId ?? null}, ${record.returnTo ?? null},
3453
3453
  ${record.callbackBindingHash ?? null}, ${record.reauthorizationId ?? null},
3454
3454
  ${record.reauthorizationRevision ?? null},
@@ -3530,6 +3530,16 @@ class PgConnectorConnectionPersistence {
3530
3530
  throw error;
3531
3531
  }
3532
3532
  }
3533
+ async listPendingConnectionRuns() {
3534
+ if (!this.scope.connectorId)
3535
+ return [];
3536
+ const rows = await this.sql`
3537
+ SELECT * FROM connector_connection_runs
3538
+ WHERE project_id = ${this.scope.projectId} AND connector_id = ${this.scope.connectorId}
3539
+ AND (status = 'running' OR (status = 'waiting' AND waiting_for = 'account_selection'))
3540
+ `;
3541
+ return rows.map(connectionRunFromRow);
3542
+ }
3533
3543
  async getConnectionRun(id) {
3534
3544
  const rows = await this.runById(id);
3535
3545
  return rows[0] ? connectionRunFromRow(rows[0]) : null;
@@ -3741,7 +3751,7 @@ function authorizationAttemptFromRow(row) {
3741
3751
  slot: row.slot,
3742
3752
  initiatedByExecutionId: row.initiated_by_execution_id,
3743
3753
  stateHash: row.state_hash,
3744
- codeVerifier: row.code_verifier,
3754
+ ...row.code_verifier === null ? {} : { codeVerifier: row.code_verifier },
3745
3755
  redirectUri: row.redirect_uri,
3746
3756
  ...row.connection_run_id === null ? {} : { connectionRunId: row.connection_run_id },
3747
3757
  ...row.return_to === null ? {} : { returnTo: row.return_to },
@@ -7081,6 +7091,55 @@ ALTER TABLE executions
7081
7091
  ON DELETE RESTRICT;
7082
7092
  `;
7083
7093
 
7094
+ // src/migrations/039-projection-source-roots.sql
7095
+ var _039_projection_source_roots_default = `ALTER TABLE ontology_sources ADD COLUMN base_materialization_id TEXT;
7096
+ ALTER TABLE ontology_sources ADD COLUMN base_commit_id TEXT;
7097
+ ALTER TABLE ontology_sources ADD CHECK ((base_materialization_id IS NULL) = (base_commit_id IS NULL));
7098
+
7099
+ -- A root selects one complete set of assertions. Unchanged roots keep their original version;
7100
+ -- activation only retires/replaces affected references, under the source commit fence.
7101
+ CREATE TABLE ontology_source_roots (
7102
+ project_id TEXT NOT NULL,
7103
+ source_id TEXT NOT NULL,
7104
+ materialization_id TEXT NOT NULL,
7105
+ root_sort_key TEXT NOT NULL,
7106
+ root_kind TEXT NOT NULL CHECK (root_kind IN ('object', 'link')),
7107
+ root_key JSONB NOT NULL,
7108
+ root JSONB NOT NULL,
7109
+ staging_ordinal BIGINT NOT NULL CHECK (staging_ordinal >= 0),
7110
+ deleted BOOLEAN NOT NULL DEFAULT FALSE,
7111
+ active BOOLEAN NOT NULL DEFAULT FALSE,
7112
+ retired_at TIMESTAMPTZ,
7113
+ PRIMARY KEY (project_id, source_id, materialization_id, root_sort_key),
7114
+ UNIQUE (project_id, source_id, materialization_id, staging_ordinal),
7115
+ FOREIGN KEY (project_id, source_id, materialization_id)
7116
+ REFERENCES ontology_sources(project_id, source_id, materialization_id) ON DELETE RESTRICT,
7117
+ CHECK (NOT active OR (NOT deleted AND retired_at IS NULL))
7118
+ );
7119
+ CREATE UNIQUE INDEX idx_ontology_source_roots_active
7120
+ ON ontology_source_roots (project_id, source_id, root_sort_key) WHERE active;
7121
+ CREATE INDEX idx_ontology_source_roots_lookup
7122
+ ON ontology_source_roots (project_id, root_sort_key, source_id) WHERE active;
7123
+ CREATE INDEX idx_ontology_source_roots_cleanup
7124
+ ON ontology_source_roots (project_id, retired_at, source_id, materialization_id, root_sort_key)
7125
+ WHERE NOT active AND retired_at IS NOT NULL;
7126
+
7127
+ INSERT INTO ontology_source_roots (
7128
+ project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root,
7129
+ staging_ordinal, active, retired_at
7130
+ )
7131
+ SELECT DISTINCT rows.project_id, rows.source_id, rows.materialization_id,
7132
+ rows.root_sort_key, rows.root_kind, rows.root_key, rows.root, rows.staging_ordinal,
7133
+ sources.status = 'active', sources.terminal_at
7134
+ FROM ontology_source_rows AS rows
7135
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id);
7136
+
7137
+ ANALYZE ontology_source_roots;
7138
+
7139
+ ALTER TABLE projection_runs ADD COLUMN source_changes_read BIGINT
7140
+ CHECK (source_changes_read IS NULL OR source_changes_read >= 0);
7141
+ `;
7142
+
7084
7143
  // src/migrations/037-execution-requester-groups.sql
7085
7144
  var _037_execution_requester_groups_default = `ALTER TABLE executions ADD COLUMN requester_group_ids JSONB NOT NULL DEFAULT '[]'::jsonb;
7086
7145
 
@@ -7105,6 +7164,20 @@ ALTER TABLE workflow_runs DROP COLUMN requester_group_ids;
7105
7164
  ALTER TABLE agent_runs DROP COLUMN requester_group_ids;
7106
7165
  `;
7107
7166
 
7167
+ // src/migrations/038-outbox-publication-order.sql
7168
+ var _038_outbox_publication_order_default = `-- Preserve the claim order without sorting every pending event for each batch.
7169
+ -- Keep the availability index for selective delayed/leased workloads.
7170
+ CREATE INDEX idx_ontology_outbox_publication_order
7171
+ ON ontology_outbox (project_id, created_at, commit_id, commit_ordinal)
7172
+ WHERE published_at IS NULL;
7173
+ `;
7174
+
7175
+ // src/migrations/040-connector-optional-pkce.sql
7176
+ var _040_connector_optional_pkce_default = `-- Existing attempts retain their encrypted S256 verifier. NULL denotes disabled PKCE.
7177
+ ALTER TABLE connector_authorization_attempts
7178
+ ALTER COLUMN code_verifier DROP NOT NULL;
7179
+ `;
7180
+
7108
7181
  // src/migrations.ts
7109
7182
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
7110
7183
  function pgSql(id, sqlText) {
@@ -7316,7 +7389,10 @@ var postgresStorageMigrations = defineMigrations({
7316
7389
  pgSql("034-device-authorizations", _034_device_authorizations_default),
7317
7390
  pgSql("035-share-grants", _035_share_grants_default),
7318
7391
  pgSql("036-share-sessions", _036_share_sessions_default),
7319
- pgSql("037-execution-requester-groups", _037_execution_requester_groups_default)
7392
+ pgSql("037-execution-requester-groups", _037_execution_requester_groups_default),
7393
+ pgSql("038-outbox-publication-order", _038_outbox_publication_order_default),
7394
+ pgSql("039-projection-source-roots", _039_projection_source_roots_default),
7395
+ pgSql("040-connector-optional-pkce", _040_connector_optional_pkce_default)
7320
7396
  ]
7321
7397
  });
7322
7398
 
@@ -9515,6 +9591,12 @@ function sourceRecord(row) {
9515
9591
  projectionRevision: row.projection_revision,
9516
9592
  ownershipHash: row.ownership_hash,
9517
9593
  ontologyRevision: row.ontology_revision,
9594
+ ...row.base_materialization_id === null ? {} : {
9595
+ base: {
9596
+ materializationId: row.base_materialization_id,
9597
+ lastCommitId: row.base_commit_id
9598
+ }
9599
+ },
9518
9600
  rootCount: row.root_count === null ? null : databaseSafeInteger(row.root_count, "Source root count"),
9519
9601
  assertionCount: row.assertion_count === null ? null : databaseSafeInteger(row.assertion_count, "Source assertion count"),
9520
9602
  createdAt: toIsoString2(row.created_at),
@@ -9683,7 +9765,7 @@ import {
9683
9765
  linkRefKey as linkRefKey3,
9684
9766
  linkRefSortKey,
9685
9767
  linkScopeSortKey as linkScopeSortKey2,
9686
- MaterializationConflictError as MaterializationConflictError5,
9768
+ MaterializationConflictError as MaterializationConflictError6,
9687
9769
  objectRefKey as objectRefKey3,
9688
9770
  objectRefSortKey,
9689
9771
  telemetryPointKey as telemetryPointKey3,
@@ -9702,7 +9784,7 @@ import {
9702
9784
  } from "@sixb/core/internal/ontology-storage-provider";
9703
9785
 
9704
9786
  // src/ontology-storage/materialization-session.ts
9705
- import { MaterializationConflictError as MaterializationConflictError3 } from "@sixb/core/internal/materialization";
9787
+ import { MaterializationConflictError as MaterializationConflictError4 } from "@sixb/core/internal/materialization";
9706
9788
  import {
9707
9789
  correlateMaterializationChunk,
9708
9790
  duplicateMaterializationWork as duplicateWork,
@@ -9716,9 +9798,9 @@ import {
9716
9798
  import {
9717
9799
  linkRefKey,
9718
9800
  linkScopeSortKey,
9719
- MaterializationConflictError as MaterializationConflictError2,
9801
+ MaterializationConflictError as MaterializationConflictError3,
9720
9802
  objectRefKey,
9721
- projectionEntityKey,
9803
+ projectionEntityKey as projectionEntityKey2,
9722
9804
  telemetryPointKey,
9723
9805
  telemetryPointSortKey
9724
9806
  } from "@sixb/core/internal/materialization";
@@ -9728,6 +9810,178 @@ import {
9728
9810
  linkSlotOverrideValue,
9729
9811
  startScopeAccumulator
9730
9812
  } from "@sixb/core/internal/ontology-storage-provider";
9813
+
9814
+ // src/ontology-storage/source-roots.ts
9815
+ import {
9816
+ MaterializationConflictError as MaterializationConflictError2,
9817
+ MaterializationValidationError,
9818
+ projectionEntityKey
9819
+ } from "@sixb/core/internal/materialization";
9820
+ import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider";
9821
+ function replacementSourceRows(sql, input) {
9822
+ const roots = input.incremental ? sql`
9823
+ SELECT roots.* FROM ontology_source_roots AS changed
9824
+ CROSS JOIN LATERAL (
9825
+ SELECT roots.* FROM ontology_source_roots AS roots
9826
+ WHERE roots.project_id = changed.project_id AND roots.source_id = changed.source_id
9827
+ AND roots.root_sort_key = changed.root_sort_key AND roots.active OFFSET 0
9828
+ ) AS roots
9829
+ WHERE changed.project_id = ${input.projectId} AND changed.source_id = ${input.sourceId}
9830
+ AND changed.materialization_id = ${input.materializationId}
9831
+ ` : sql`SELECT * FROM ontology_source_roots WHERE project_id = ${input.projectId} AND source_id = ${input.sourceId} AND active`;
9832
+ return sql`
9833
+ SELECT * FROM ontology_source_rows WHERE project_id = ${input.projectId}
9834
+ AND source_id = ${input.sourceId} AND materialization_id = ${input.materializationId}
9835
+ UNION ALL
9836
+ SELECT rows.* FROM (${roots}) AS roots
9837
+ CROSS JOIN LATERAL (
9838
+ SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.project_id = roots.project_id
9839
+ AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
9840
+ AND rows.root_sort_key = roots.root_sort_key OFFSET 0
9841
+ ) AS rows
9842
+ `;
9843
+ }
9844
+ async function stageSourceRoots(sql, manifest, input) {
9845
+ const roots = sourceStageRoots(manifest.projection_kind, input);
9846
+ if (roots.length === 0)
9847
+ return;
9848
+ if (!manifest.base_materialization_id && roots.some((root) => root.deleted)) {
9849
+ throw new MaterializationValidationError("Root deletions require a source delta base.");
9850
+ }
9851
+ const values = jsonParameter2(sql, roots.map((root) => ({
9852
+ root_sort_key: root.sortKey,
9853
+ root_kind: root.root.kind,
9854
+ root_key: JSON.parse(root.rootKey),
9855
+ root: root.root,
9856
+ staging_ordinal: root.stagingOrdinal,
9857
+ deleted: root.deleted
9858
+ })));
9859
+ const inserted = await sql`
9860
+ INSERT INTO ontology_source_roots (
9861
+ project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
9862
+ ) SELECT ${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
9863
+ root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
9864
+ FROM jsonb_to_recordset(${values}) AS input(
9865
+ root_sort_key TEXT, root_kind TEXT, root_key JSONB, root JSONB, staging_ordinal BIGINT, deleted BOOLEAN)
9866
+ ON CONFLICT DO NOTHING RETURNING root_sort_key
9867
+ `;
9868
+ if (inserted.length === roots.length)
9869
+ return;
9870
+ const insertedKeys = new Set(inserted.map((row) => row.root_sort_key));
9871
+ const skipped = roots.filter((root) => !insertedKeys.has(root.sortKey));
9872
+ const stored = await sql`
9873
+ SELECT root_sort_key, staging_ordinal, deleted FROM ontology_source_roots
9874
+ WHERE project_id = ${input.projectId} AND source_id = ${input.source.projectionId}
9875
+ AND materialization_id = ${input.materializationId}
9876
+ AND root_sort_key = ANY(${sql.array(skipped.map((root) => root.sortKey))}::text[])
9877
+ `;
9878
+ const existing = new Map(stored.map((root) => [root.root_sort_key, root]));
9879
+ if (skipped.some((root) => {
9880
+ const previous = existing.get(root.sortKey);
9881
+ return !previous || Number(previous.staging_ordinal) !== root.stagingOrdinal || previous.deleted !== root.deleted;
9882
+ }))
9883
+ throw new MaterializationValidationError("Source materialization repeats root or stream ordinal with different content.");
9884
+ }
9885
+ async function activateSourceRoots(sql, projectId, candidate, activation) {
9886
+ if (candidate.base_materialization_id !== null && (candidate.base_materialization_id !== activation.expected.activeMaterializationId || candidate.base_commit_id !== activation.expected.lastCommitId))
9887
+ throw new MaterializationConflictError2("projection-fence", "Source delta base changed before activation.");
9888
+ if (candidate.base_materialization_id !== null && Number(candidate.root_count) === 0)
9889
+ return;
9890
+ const affected = candidate.base_materialization_id === null ? sql`` : sql`AND root_sort_key IN (
9891
+ SELECT root_sort_key FROM ontology_source_roots
9892
+ WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
9893
+ AND materialization_id = ${activation.materializationId}
9894
+ )`;
9895
+ await sql`
9896
+ UPDATE ontology_source_roots AS roots SET active = FALSE, retired_at = ${activation.updatedAt}
9897
+ WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId} AND active
9898
+ ${affected}
9899
+ `;
9900
+ await sql`
9901
+ UPDATE ontology_source_roots SET active = NOT deleted,
9902
+ retired_at = CASE WHEN deleted THEN ${activation.updatedAt}::timestamptz ELSE NULL END
9903
+ WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
9904
+ AND materialization_id = ${activation.materializationId}
9905
+ `;
9906
+ }
9907
+ async function assertSourceRootCoverage(sql, manifest) {
9908
+ const [invalid] = await sql`
9909
+ SELECT 1 FROM ontology_source_roots AS roots
9910
+ WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
9911
+ AND materialization_id = ${manifest.materialization_id}
9912
+ AND roots.deleted = EXISTS (
9913
+ SELECT 1 FROM ontology_source_rows AS rows
9914
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
9915
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
9916
+ ) LIMIT 1
9917
+ `;
9918
+ if (invalid)
9919
+ throw new MaterializationValidationError("Source roots must have complete assertions or an explicit deletion, never both.");
9920
+ }
9921
+ function activeSourceRows(sql, projectId, refs) {
9922
+ const keys = refs.flatMap((entity) => {
9923
+ const entitySortKey = utf8SortKey(projectionEntityKey(entity));
9924
+ const roots = entity.kind === "object" ? [entitySortKey] : [
9925
+ entitySortKey,
9926
+ utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source }))
9927
+ ];
9928
+ return roots.map((rootSortKey) => ({
9929
+ root_sort_key: rootSortKey,
9930
+ entity_sort_key: entitySortKey
9931
+ }));
9932
+ });
9933
+ return sql`
9934
+ SELECT rows.* FROM jsonb_to_recordset(${jsonParameter2(sql, keys)}) AS requested(root_sort_key TEXT, entity_sort_key TEXT)
9935
+ JOIN ontology_source_roots AS roots ON roots.project_id = ${projectId}
9936
+ AND roots.active AND roots.root_sort_key = requested.root_sort_key
9937
+ JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
9938
+ AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
9939
+ AND rows.root_sort_key = roots.root_sort_key
9940
+ `;
9941
+ }
9942
+ function replacementAssertionRows(sql, input) {
9943
+ const keys = input.refs.map((ref) => ({
9944
+ entity_kind: ref.kind,
9945
+ entity_key: JSON.parse(projectionEntityKey(ref)),
9946
+ root_keys: ref.kind === "object" ? [utf8SortKey(projectionEntityKey(ref))] : [
9947
+ utf8SortKey(projectionEntityKey(ref)),
9948
+ utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source }))
9949
+ ]
9950
+ }));
9951
+ const previous = !input.includePrevious ? sql`` : sql`UNION ALL
9952
+ SELECT rows.* FROM (
9953
+ SELECT roots.* FROM ontology_source_roots AS roots
9954
+ WHERE roots.project_id = ${input.projectId} AND roots.source_id = ${input.sourceId}
9955
+ AND roots.active AND roots.root_sort_key = ANY(requested.root_keys)
9956
+ ${!input.incremental ? sql`` : sql`AND EXISTS (SELECT 1 FROM ontology_source_roots AS changed
9957
+ WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
9958
+ AND changed.materialization_id = ${input.materializationId} AND changed.root_sort_key = roots.root_sort_key)`}
9959
+
9960
+ OFFSET 0
9961
+ ) AS roots
9962
+ CROSS JOIN LATERAL (
9963
+ SELECT rows.* FROM ontology_source_rows AS rows
9964
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
9965
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
9966
+ AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
9967
+ OFFSET 0
9968
+ ) AS rows
9969
+ `;
9970
+ return sql`
9971
+ SELECT selected.* FROM jsonb_to_recordset(${jsonParameter2(sql, keys)})
9972
+ AS requested(entity_kind TEXT, entity_key JSONB, root_keys TEXT[])
9973
+ CROSS JOIN LATERAL (
9974
+ SELECT rows.* FROM ontology_source_rows AS rows
9975
+ WHERE rows.project_id = ${input.projectId} AND rows.source_id = ${input.sourceId}
9976
+ AND rows.materialization_id = ${input.materializationId}
9977
+ AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
9978
+ ${previous}
9979
+ OFFSET 0
9980
+ ) AS selected
9981
+ `;
9982
+ }
9983
+
9984
+ // src/ontology-storage/materialization-state.ts
9731
9985
  var PG_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work";
9732
9986
  var PG_REPLACEMENT_WORK_TABLE = "ontology_replacement_work";
9733
9987
 
@@ -9760,22 +10014,7 @@ class PgMaterializationStateReader {
9760
10014
  JOIN requested USING (object_type_id, primary_id)
9761
10015
  WHERE objects.project_id = ${this.projectId}
9762
10016
  `,
9763
- this.sql`
9764
- WITH requested AS (
9765
- SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
9766
- AS requested_values(object_type_id TEXT, primary_id TEXT)
9767
- )
9768
- SELECT rows.*
9769
- FROM ontology_source_rows AS rows
9770
- JOIN requested USING (object_type_id, primary_id)
9771
- JOIN ontology_sources AS sources
9772
- ON sources.project_id = rows.project_id
9773
- AND sources.source_id = rows.source_id
9774
- AND sources.materialization_id = rows.materialization_id
9775
- WHERE rows.project_id = ${this.projectId}
9776
- AND rows.entity_kind = 'object'
9777
- AND sources.status = 'active'
9778
- `,
10017
+ activeSourceRows(this.sql, this.projectId, refs.map((ref) => ({ kind: "object", ref }))),
9779
10018
  this.sql`
9780
10019
  WITH requested AS (
9781
10020
  SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
@@ -9816,7 +10055,7 @@ class PgMaterializationStateReader {
9816
10055
  const effectiveRow = effective.get(key);
9817
10056
  return {
9818
10057
  ref: structuredClone(ref),
9819
- source: sourceObject(sources.get(projectionEntityKey({ kind: "object", ref }))),
10058
+ source: sourceObject(sources.get(projectionEntityKey2({ kind: "object", ref }))),
9820
10059
  override: overrides.get(key) ?? null,
9821
10060
  effective: effectiveRow ? effectiveObjectSnapshot(effectiveRow) : null,
9822
10061
  latestTelemetry: telemetry.get(key) ?? []
@@ -9852,26 +10091,7 @@ class PgMaterializationStateReader {
9852
10091
  JOIN requested USING (source_type_id, source_id, link_id, target_type_id, target_id)
9853
10092
  WHERE links.project_id = ${this.projectId}
9854
10093
  `,
9855
- this.sql`
9856
- WITH requested AS (
9857
- SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
9858
- source_type_id TEXT, source_primary_id TEXT, link_id TEXT,
9859
- target_type_id TEXT, target_primary_id TEXT
9860
- )
9861
- )
9862
- SELECT rows.*
9863
- FROM ontology_source_rows AS rows
9864
- JOIN requested USING (
9865
- source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
9866
- )
9867
- JOIN ontology_sources AS sources
9868
- ON sources.project_id = rows.project_id
9869
- AND sources.source_id = rows.source_id
9870
- AND sources.materialization_id = rows.materialization_id
9871
- WHERE rows.project_id = ${this.projectId}
9872
- AND rows.entity_kind = 'link'
9873
- AND sources.status = 'active'
9874
- `,
10094
+ activeSourceRows(this.sql, this.projectId, refs.map((ref) => ({ kind: "link", ref }))),
9875
10095
  this.sql`
9876
10096
  WITH requested AS (
9877
10097
  SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
@@ -9910,7 +10130,7 @@ class PgMaterializationStateReader {
9910
10130
  const effectiveRow = effective.get(key);
9911
10131
  return {
9912
10132
  ref: structuredClone(ref),
9913
- source: sourceLink(sources.get(projectionEntityKey({ kind: "link", ref }))),
10133
+ source: sourceLink(sources.get(projectionEntityKey2({ kind: "link", ref }))),
9914
10134
  override: overrides.get(key) ?? null,
9915
10135
  slotOverride: slotOverrides.get(linkScopeSortKey(ref.source, ref.linkId)) ?? null,
9916
10136
  effective: effectiveRow ? effectiveLinkSnapshot(effectiveRow) : null
@@ -9979,13 +10199,14 @@ class PgMaterializationStateReader {
9979
10199
  ON requested.source_type_id = rows.source_type_id
9980
10200
  AND requested.source_id = rows.source_primary_id
9981
10201
  AND requested.link_id = rows.link_id
9982
- JOIN ontology_sources AS sources
10202
+ JOIN ontology_source_roots AS sources
9983
10203
  ON sources.project_id = rows.project_id
9984
10204
  AND sources.source_id = rows.source_id
9985
10205
  AND sources.materialization_id = rows.materialization_id
10206
+ AND sources.root_sort_key = rows.root_sort_key
9986
10207
  WHERE rows.project_id = ${this.projectId}
9987
10208
  AND rows.entity_kind = 'link'
9988
- AND sources.status = 'active'
10209
+ AND sources.active
9989
10210
  `,
9990
10211
  this.sql`
9991
10212
  WITH requested AS (
@@ -10058,7 +10279,7 @@ class PgMaterializationStateReader {
10058
10279
  for (const row of rows) {
10059
10280
  const accumulator = accumulators.get(row.scope_sort_key);
10060
10281
  if (!accumulator) {
10061
- throw new MaterializationConflictError2("effective-state", `Unexpected link scope '${row.scope_sort_key}' returned by storage.`);
10282
+ throw new MaterializationConflictError3("effective-state", `Unexpected link scope '${row.scope_sort_key}' returned by storage.`);
10062
10283
  }
10063
10284
  const snapshot = effectiveLinkSnapshot(row);
10064
10285
  appendScopeSnapshot(accumulator, snapshot);
@@ -10127,30 +10348,32 @@ class PgMaterializationStateReader {
10127
10348
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
10128
10349
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
10129
10350
  FROM ontology_source_rows AS rows
10130
- JOIN ontology_sources AS sources
10351
+ JOIN ontology_source_roots AS sources
10131
10352
  ON sources.project_id = rows.project_id
10132
10353
  AND sources.source_id = rows.source_id
10133
10354
  AND sources.materialization_id = rows.materialization_id
10355
+ AND sources.root_sort_key = rows.root_sort_key
10134
10356
  JOIN requested
10135
10357
  ON requested.object_type_id = rows.source_type_id
10136
10358
  AND requested.primary_id = rows.source_primary_id
10137
10359
  WHERE rows.project_id = ${this.projectId}
10138
10360
  AND rows.entity_kind = 'link'
10139
- AND sources.status = 'active'
10361
+ AND sources.active
10140
10362
  UNION
10141
10363
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
10142
10364
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
10143
10365
  FROM ontology_source_rows AS rows
10144
- JOIN ontology_sources AS sources
10366
+ JOIN ontology_source_roots AS sources
10145
10367
  ON sources.project_id = rows.project_id
10146
10368
  AND sources.source_id = rows.source_id
10147
10369
  AND sources.materialization_id = rows.materialization_id
10370
+ AND sources.root_sort_key = rows.root_sort_key
10148
10371
  JOIN requested
10149
10372
  ON requested.object_type_id = rows.target_type_id
10150
10373
  AND requested.primary_id = rows.target_primary_id
10151
10374
  WHERE rows.project_id = ${this.projectId}
10152
10375
  AND rows.entity_kind = 'link'
10153
- AND sources.status = 'active'
10376
+ AND sources.active
10154
10377
  ), selected AS (
10155
10378
  SELECT DISTINCT links.*,
10156
10379
  ${this.sql.unsafe(linkSortExpression("links"))} AS sort_key
@@ -10169,13 +10392,15 @@ class PgMaterializationStateReader {
10169
10392
  }
10170
10393
  async* replacementIdentities(input) {
10171
10394
  await this.prepareReplacementIdentities(input);
10395
+ await this.sql`ANALYZE ${this.sql(PG_REPLACEMENT_WORK_TABLE)}`;
10172
10396
  let cursor = null;
10173
10397
  while (true) {
10398
+ const after = cursor === null ? this.sql`` : this.sql`AND sort_key > ${cursor}`;
10174
10399
  const rows = await this.sql`
10175
10400
  SELECT entity_kind, identity_key, sort_key, diff_required
10176
10401
  FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)}
10177
10402
  WHERE session_id = ${input.sessionId} AND entity_kind = ${input.kind}
10178
- AND (${cursor}::text IS NULL OR sort_key > ${cursor})
10403
+ ${after}
10179
10404
  ORDER BY sort_key
10180
10405
  LIMIT ${input.pageRows}
10181
10406
  `;
@@ -10190,19 +10415,19 @@ class PgMaterializationStateReader {
10190
10415
  const candidates = await this.replacementSources(sourceId, [candidateMaterializationId], refs.map((ref) => ({ kind: "object", ref })));
10191
10416
  return base.map((state) => ({
10192
10417
  ref: state.ref,
10193
- candidateSource: sourceObject(candidates.byMaterialization.get(candidateMaterializationId)?.get(projectionEntityKey({ kind: "object", ref: state.ref }))),
10418
+ candidateSource: sourceObject(candidates.byMaterialization.get(candidateMaterializationId)?.get(projectionEntityKey2({ kind: "object", ref: state.ref }))),
10194
10419
  override: state.override,
10195
10420
  effective: state.effective,
10196
10421
  latestTelemetry: state.latestTelemetry
10197
10422
  }));
10198
10423
  }
10199
- async replacementLinkStates(sourceId, candidateMaterializationId, materializationIds, identities) {
10424
+ async replacementLinkStates(sourceId, candidateMaterializationId, materializationIds, identities, incremental = false) {
10200
10425
  const refs = identities.map((identity) => identity.ref);
10201
10426
  const base = await this.linkStates(refs);
10202
- const replacements = await this.replacementSources(sourceId, materializationIds, refs.map((ref) => ({ kind: "link", ref })));
10427
+ const replacements = await this.replacementSources(sourceId, materializationIds, refs.map((ref) => ({ kind: "link", ref })), incremental);
10203
10428
  const candidate = replacements.byMaterialization.get(candidateMaterializationId);
10204
10429
  return base.map((state, index) => {
10205
- const key = projectionEntityKey({ kind: "link", ref: state.ref });
10430
+ const key = projectionEntityKey2({ kind: "link", ref: state.ref });
10206
10431
  return {
10207
10432
  ref: state.ref,
10208
10433
  candidateSource: replacements.owned.has(key) ? sourceLink(candidate?.get(key)) : state.source,
@@ -10269,33 +10494,23 @@ class PgMaterializationStateReader {
10269
10494
  `;
10270
10495
  return new Map(rows.map((row) => [linkRefKey(linkRefFromColumns(row)), row.last_commit_id]));
10271
10496
  }
10272
- async replacementSources(sourceId, materializationIds, refs) {
10497
+ async replacementSources(sourceId, materializationIds, refs, incremental = false) {
10273
10498
  if (materializationIds.length === 0 || refs.length === 0) {
10274
10499
  return { owned: new Set, byMaterialization: new Map };
10275
10500
  }
10276
- const keys = refs.map((ref) => ({
10277
- entity_kind: ref.kind,
10278
- entity_key: JSON.parse(projectionEntityKey(ref))
10279
- }));
10280
- const rows = await this.sql`
10281
- WITH requested AS (
10282
- SELECT *
10283
- FROM jsonb_to_recordset(${jsonParameter2(this.sql, keys)})
10284
- AS requested_values(entity_kind TEXT, entity_key JSONB)
10285
- )
10286
- SELECT rows.* FROM ontology_source_rows AS rows
10287
- JOIN requested USING (entity_kind, entity_key)
10288
- WHERE rows.project_id = ${this.projectId}
10289
- AND rows.source_id = ${sourceId}
10290
- AND rows.materialization_id = ANY(
10291
- ${this.sql.array([...new Set(materializationIds)])}::text[]
10292
- )
10293
- `;
10501
+ const rows = await replacementAssertionRows(this.sql, {
10502
+ projectId: this.projectId,
10503
+ sourceId,
10504
+ materializationId: materializationIds[0],
10505
+ incremental,
10506
+ includePrevious: materializationIds.length > 1,
10507
+ refs
10508
+ });
10294
10509
  const owned = new Set;
10295
10510
  const byMaterialization = new Map;
10296
10511
  for (const row of rows) {
10297
10512
  const source = storedSource(row);
10298
- const key = projectionEntityKey(source.assertion);
10513
+ const key = projectionEntityKey2(source.assertion);
10299
10514
  owned.add(key);
10300
10515
  const materialization = byMaterialization.get(row.materialization_id) ?? new Map;
10301
10516
  materialization.set(key, source);
@@ -10311,20 +10526,19 @@ class PgMaterializationStateReader {
10311
10526
  await this.prepareReplacementLinks(input);
10312
10527
  }
10313
10528
  async prepareReplacementObjects(input) {
10314
- const materializationIds = [
10315
- input.candidateMaterializationId,
10316
- ...input.previousMaterializationId ? [input.previousMaterializationId] : []
10317
- ];
10529
+ const rows = replacementSourceRows(this.sql, {
10530
+ projectId: this.projectId,
10531
+ sourceId: input.sourceId,
10532
+ materializationId: input.candidateMaterializationId,
10533
+ incremental: input.incremental ?? false
10534
+ });
10318
10535
  await this.sql`
10319
10536
  WITH selected AS (
10320
10537
  SELECT DISTINCT object_type_id, primary_id,
10321
10538
  ${this.sql.unsafe(objectKeyExpression("rows"))} AS identity_key,
10322
10539
  ${this.sql.unsafe(objectSortExpression("rows"))} AS sort_key
10323
- FROM ontology_source_rows AS rows
10324
- WHERE project_id = ${this.projectId}
10325
- AND source_id = ${input.sourceId}
10326
- AND entity_kind = 'object'
10327
- AND materialization_id = ANY(${this.sql.array(materializationIds)}::text[])
10540
+ FROM (${rows}) AS rows
10541
+ WHERE entity_kind = 'object'
10328
10542
  )
10329
10543
  INSERT INTO ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
10330
10544
  session_id, entity_kind, identity_key, sort_key, diff_required
@@ -10337,10 +10551,13 @@ class PgMaterializationStateReader {
10337
10551
  `;
10338
10552
  }
10339
10553
  async prepareReplacementLinks(input) {
10340
- const materializationIds = [
10341
- input.candidateMaterializationId,
10342
- ...input.previousMaterializationId ? [input.previousMaterializationId] : []
10343
- ];
10554
+ await this.sql`ANALYZE ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}`;
10555
+ const rows = replacementSourceRows(this.sql, {
10556
+ projectId: this.projectId,
10557
+ sourceId: input.sourceId,
10558
+ materializationId: input.candidateMaterializationId,
10559
+ incremental: input.incremental ?? false
10560
+ });
10344
10561
  await this.sql`
10345
10562
  WITH incident_objects AS (
10346
10563
  SELECT DISTINCT payload->'ref'->>'objectTypeId' AS object_type_id,
@@ -10350,11 +10567,8 @@ class PgMaterializationStateReader {
10350
10567
  ), replacement_links AS (
10351
10568
  SELECT source_type_id, source_primary_id AS source_id, link_id,
10352
10569
  target_type_id, target_primary_id AS target_id
10353
- FROM ontology_source_rows
10354
- WHERE project_id = ${this.projectId}
10355
- AND source_id = ${input.sourceId}
10356
- AND entity_kind = 'link'
10357
- AND materialization_id = ANY(${this.sql.array(materializationIds)}::text[])
10570
+ FROM (${rows}) AS rows
10571
+ WHERE entity_kind = 'link'
10358
10572
  ), incident_links AS (
10359
10573
  SELECT links.source_type_id, links.source_id, links.link_id,
10360
10574
  links.target_type_id, links.target_id
@@ -10393,30 +10607,32 @@ class PgMaterializationStateReader {
10393
10607
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
10394
10608
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
10395
10609
  FROM ontology_source_rows AS rows
10396
- JOIN ontology_sources AS sources
10610
+ JOIN ontology_source_roots AS sources
10397
10611
  ON sources.project_id = rows.project_id
10398
10612
  AND sources.source_id = rows.source_id
10399
10613
  AND sources.materialization_id = rows.materialization_id
10614
+ AND sources.root_sort_key = rows.root_sort_key
10400
10615
  JOIN incident_objects
10401
10616
  ON incident_objects.object_type_id = rows.source_type_id
10402
10617
  AND incident_objects.primary_id = rows.source_primary_id
10403
10618
  WHERE rows.project_id = ${this.projectId}
10404
10619
  AND rows.entity_kind = 'link'
10405
- AND sources.status = 'active'
10620
+ AND sources.active
10406
10621
  UNION
10407
10622
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
10408
10623
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
10409
10624
  FROM ontology_source_rows AS rows
10410
- JOIN ontology_sources AS sources
10625
+ JOIN ontology_source_roots AS sources
10411
10626
  ON sources.project_id = rows.project_id
10412
10627
  AND sources.source_id = rows.source_id
10413
10628
  AND sources.materialization_id = rows.materialization_id
10629
+ AND sources.root_sort_key = rows.root_sort_key
10414
10630
  JOIN incident_objects
10415
10631
  ON incident_objects.object_type_id = rows.target_type_id
10416
10632
  AND incident_objects.primary_id = rows.target_primary_id
10417
10633
  WHERE rows.project_id = ${this.projectId}
10418
10634
  AND rows.entity_kind = 'link'
10419
- AND sources.status = 'active'
10635
+ AND sources.active
10420
10636
  ), diff_links AS (
10421
10637
  SELECT * FROM replacement_links
10422
10638
  UNION SELECT * FROM incident_links
@@ -10484,9 +10700,9 @@ function activeSourceMap(rows) {
10484
10700
  const result = new Map;
10485
10701
  for (const row of rows) {
10486
10702
  const source = storedSource(row);
10487
- const key = projectionEntityKey(source.assertion);
10703
+ const key = projectionEntityKey2(source.assertion);
10488
10704
  if (result.has(key)) {
10489
- throw new MaterializationConflictError2("source-materialization", `Multiple active sources assert ${key}.`);
10705
+ throw new MaterializationConflictError3("source-materialization", `Multiple active sources assert ${key}.`);
10490
10706
  }
10491
10707
  result.set(key, source);
10492
10708
  }
@@ -10501,7 +10717,7 @@ function activeScopeSourceMap(rows) {
10501
10717
  const ref = source.assertion.ref;
10502
10718
  const key = linkScopeSortKey(ref.source, ref.linkId);
10503
10719
  if (result.has(key)) {
10504
- throw new MaterializationConflictError2("source-materialization", `Multiple active sources assert cardinality-one link slot ${key}.`);
10720
+ throw new MaterializationConflictError3("source-materialization", `Multiple active sources assert cardinality-one link slot ${key}.`);
10505
10721
  }
10506
10722
  result.set(key, source);
10507
10723
  }
@@ -10565,7 +10781,7 @@ function storedSource(row) {
10565
10781
  }
10566
10782
  function effectiveObjectSnapshot(row) {
10567
10783
  if (!row.last_commit_id) {
10568
- throw new MaterializationConflictError2("effective-state", `Effective object ${row.object_type_id}:${row.primary_id} lacks materializer provenance.`);
10784
+ throw new MaterializationConflictError3("effective-state", `Effective object ${row.object_type_id}:${row.primary_id} lacks materializer provenance.`);
10569
10785
  }
10570
10786
  return {
10571
10787
  ref: objectRefFromColumns(row),
@@ -10578,7 +10794,7 @@ function effectiveObjectSnapshot(row) {
10578
10794
  }
10579
10795
  function effectiveLinkSnapshot(row) {
10580
10796
  if (!row.last_commit_id) {
10581
- throw new MaterializationConflictError2("effective-state", `Effective link ${linkRefKey(linkRefFromColumns(row))} lacks materializer provenance.`);
10797
+ throw new MaterializationConflictError3("effective-state", `Effective link ${linkRefKey(linkRefFromColumns(row))} lacks materializer provenance.`);
10582
10798
  }
10583
10799
  return {
10584
10800
  ref: linkRefFromColumns(row),
@@ -10593,7 +10809,7 @@ function effectiveLinkSnapshot(row) {
10593
10809
  function storedPoint(row) {
10594
10810
  const at = toIsoString2(row.at);
10595
10811
  if (!row.last_commit_id) {
10596
- throw new MaterializationConflictError2("timeseries-point", `Telemetry point ${telemetryPointSortKey({
10812
+ throw new MaterializationConflictError3("timeseries-point", `Telemetry point ${telemetryPointSortKey({
10597
10813
  object: { objectTypeId: row.object_type_id, primaryId: row.object_id },
10598
10814
  propertyId: row.property_id
10599
10815
  }, at)} lacks materializer provenance.`);
@@ -10652,6 +10868,9 @@ function jsonTupleExpression(parts) {
10652
10868
 
10653
10869
  // src/ontology-storage/materialization-session.ts
10654
10870
  class PgMaterializationSessionState extends ProviderMaterializationSessionState {
10871
+ stagedWorkCount = 0;
10872
+ changedObjects = 0;
10873
+ changedLinks = 0;
10655
10874
  appliedPlanCursor = null;
10656
10875
  appliedEventCursor = null;
10657
10876
  }
@@ -10668,7 +10887,7 @@ class PgMaterializationSessions {
10668
10887
  }
10669
10888
  async create(header) {
10670
10889
  if (!this.context?.active) {
10671
- throw new MaterializationConflictError3("effective-state", "Materialization sessions require an active storage transaction.");
10890
+ throw new MaterializationConflictError4("effective-state", "Materialization sessions require an active storage transaction.");
10672
10891
  }
10673
10892
  await this.ensureTables();
10674
10893
  const session = new PgMaterializationSessionState(structuredClone(header), this.context.id);
@@ -10680,7 +10899,7 @@ class PgMaterializationSessions {
10680
10899
  require(session) {
10681
10900
  const value = this.sessions.get(session.providerToken);
10682
10901
  if (!value || !value.active || !this.context?.active || value.transactionId !== this.context.id) {
10683
- throw new MaterializationConflictError3("effective-state", "Materialization session is inactive.");
10902
+ throw new MaterializationConflictError4("effective-state", "Materialization session is inactive.");
10684
10903
  }
10685
10904
  return value;
10686
10905
  }
@@ -10738,14 +10957,18 @@ class PgMaterializationSessions {
10738
10957
  }
10739
10958
  throw error;
10740
10959
  }
10960
+ session.stagedWorkCount += records.length;
10741
10961
  }
10742
10962
  async* stream(input) {
10743
10963
  const session = this.require(input.session);
10744
10964
  const stream = session.workStreams[input.order];
10745
10965
  if (stream.started) {
10746
- throw new MaterializationConflictError3("effective-state", `Materialization ${input.order} work may only be streamed once per session.`);
10966
+ throw new MaterializationConflictError4("effective-state", `Materialization ${input.order} work may only be streamed once per session.`);
10747
10967
  }
10748
10968
  stream.started = true;
10969
+ if (!session.workSealed && session.stagedWorkCount >= 1e4) {
10970
+ await this.sql`ANALYZE ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}`;
10971
+ }
10749
10972
  session.workSealed = true;
10750
10973
  let cursor = null;
10751
10974
  while (true) {
@@ -10766,10 +10989,13 @@ class PgMaterializationSessions {
10766
10989
  return [];
10767
10990
  const keys = refs.map((ref) => `object-existence:${JSON.stringify([ref.objectTypeId, ref.primaryId])}`);
10768
10991
  const rows = await this.sql`
10769
- SELECT unique_key, payload FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
10770
- WHERE session_id = ${session.id}
10771
- AND unique_key = ANY(${this.sql.array(keys)}::text[])
10772
- AND kind = 'object-existence'
10992
+ SELECT work.unique_key, work.payload
10993
+ FROM unnest(${this.sql.array(keys)}::text[]) AS requested(unique_key)
10994
+ CROSS JOIN LATERAL (
10995
+ SELECT unique_key, payload FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
10996
+ WHERE session_id = ${session.id} AND unique_key = requested.unique_key
10997
+ OFFSET 0
10998
+ ) AS work
10773
10999
  `;
10774
11000
  const found = new Map(rows.map((row) => [row.unique_key, row.payload]));
10775
11001
  return keys.flatMap((key) => {
@@ -10777,6 +11003,9 @@ class PgMaterializationSessions {
10777
11003
  if (!payload)
10778
11004
  return [];
10779
11005
  const record = structuredClone(payload);
11006
+ if (record.kind !== "object-existence") {
11007
+ invalidCorrelation("Object existence work has an invalid kind.");
11008
+ }
10780
11009
  return [{ ref: record.ref, exists: record.exists }];
10781
11010
  });
10782
11011
  }
@@ -10931,7 +11160,7 @@ function workCursor(row) {
10931
11160
  // src/ontology-storage/materialization-writer.ts
10932
11161
  import {
10933
11162
  linkRefKey as linkRefKey2,
10934
- MaterializationConflictError as MaterializationConflictError4,
11163
+ MaterializationConflictError as MaterializationConflictError5,
10935
11164
  objectRefKey as objectRefKey2,
10936
11165
  telemetryPointKey as telemetryPointKey2
10937
11166
  } from "@sixb/core/internal/materialization";
@@ -11447,7 +11676,7 @@ function effectiveLinkConflict(item) {
11447
11676
  return effectiveConflict(`Expected link ${linkIdentityKey(item)} changed.`);
11448
11677
  }
11449
11678
  function pointConflict(item) {
11450
- return new MaterializationConflictError4("timeseries-point", `Telemetry point ${telemetryPointKey2({
11679
+ return new MaterializationConflictError5("timeseries-point", `Telemetry point ${telemetryPointKey2({
11451
11680
  object: { objectTypeId: item.objectTypeId, primaryId: item.objectId },
11452
11681
  propertyId: item.propertyId
11453
11682
  }, item.at)} changed.`);
@@ -11482,14 +11711,14 @@ class PgOntologyMaterializationStorage {
11482
11711
  const linkScopeRevisions = await reader.linkScopeRevisions(input.expected.linkScopes);
11483
11712
  for (const [index, expected] of input.expected.linkScopes.entries()) {
11484
11713
  if (linkScopeRevisions[index]?.fingerprint !== expected.fingerprint) {
11485
- throw new MaterializationConflictError5("effective-state", `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`);
11714
+ throw new MaterializationConflictError6("effective-state", `Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`);
11486
11715
  }
11487
11716
  }
11488
11717
  const points = await reader.exactPoints(input.expected.points, true);
11489
11718
  const pointRevisions = new Map(points.map((point) => [telemetryPointKey3(point.series, point.at), point.lastCommitId]));
11490
11719
  for (const expected of input.expected.points) {
11491
11720
  if ((pointRevisions.get(telemetryPointKey3(expected.series, expected.at)) ?? null) !== expected.lastCommitId) {
11492
- throw new MaterializationConflictError5("timeseries-point", `Telemetry point ${telemetryPointKey3(expected.series, expected.at)} changed.`);
11721
+ throw new MaterializationConflictError6("timeseries-point", `Telemetry point ${telemetryPointKey3(expected.series, expected.at)} changed.`);
11493
11722
  }
11494
11723
  }
11495
11724
  return session.publicSession();
@@ -11548,10 +11777,10 @@ class PgOntologyMaterializationStorage {
11548
11777
  const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId);
11549
11778
  if (input.entityKind === "object") {
11550
11779
  if (replacement.projectionKind !== "object") {
11551
- throw new MaterializationConflictError5("source-materialization", "Link projection replacement cannot stream object state.");
11780
+ throw new MaterializationConflictError6("source-materialization", "Link projection replacement cannot stream object state.");
11552
11781
  }
11553
11782
  if (replacement.objectStreamStarted) {
11554
- throw new MaterializationConflictError5("effective-state", "Replacement object state may only be streamed once per session.");
11783
+ throw new MaterializationConflictError6("effective-state", "Replacement object state may only be streamed once per session.");
11555
11784
  }
11556
11785
  replacement.objectStreamStarted = true;
11557
11786
  for await (const identities of reader.replacementIdentities({
@@ -11559,6 +11788,7 @@ class PgOntologyMaterializationStorage {
11559
11788
  sourceId: replacement.sourceId,
11560
11789
  candidateMaterializationId: replacement.candidateMaterializationId,
11561
11790
  previousMaterializationId: replacement.previousMaterializationId,
11791
+ incremental: replacement.incremental,
11562
11792
  kind: "object",
11563
11793
  pageRows: input.pageRows
11564
11794
  })) {
@@ -11577,10 +11807,10 @@ class PgOntologyMaterializationStorage {
11577
11807
  return;
11578
11808
  }
11579
11809
  if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
11580
- throw new MaterializationConflictError5("effective-state", "Object projection replacement must fully stream object state before link state.");
11810
+ throw new MaterializationConflictError6("effective-state", "Object projection replacement must fully stream object state before link state.");
11581
11811
  }
11582
11812
  if (replacement.linkStreamStarted) {
11583
- throw new MaterializationConflictError5("effective-state", "Replacement link state may only be streamed once per session.");
11813
+ throw new MaterializationConflictError6("effective-state", "Replacement link state may only be streamed once per session.");
11584
11814
  }
11585
11815
  replacement.linkStreamStarted = true;
11586
11816
  const materializationIds = [
@@ -11592,6 +11822,7 @@ class PgOntologyMaterializationStorage {
11592
11822
  sourceId: replacement.sourceId,
11593
11823
  candidateMaterializationId: replacement.candidateMaterializationId,
11594
11824
  previousMaterializationId: replacement.previousMaterializationId,
11825
+ incremental: replacement.incremental,
11595
11826
  kind: "link",
11596
11827
  pageRows: input.pageRows
11597
11828
  })) {
@@ -11602,7 +11833,7 @@ class PgOntologyMaterializationStorage {
11602
11833
  }
11603
11834
  return identity;
11604
11835
  });
11605
- const links = await reader.replacementLinkStates(replacement.sourceId, replacement.candidateMaterializationId, materializationIds, linkIdentities);
11836
+ const links = await reader.replacementLinkStates(replacement.sourceId, replacement.candidateMaterializationId, materializationIds, linkIdentities, replacement.incremental);
11606
11837
  yield { objects: [], links };
11607
11838
  }
11608
11839
  this.sessions.require(input.session);
@@ -11625,6 +11856,8 @@ class PgOntologyMaterializationStorage {
11625
11856
  assertPlanChunkCorrelations(input.chunk, commit);
11626
11857
  const progress = await this.sessions.prepareChunkSequence(session, input.chunk);
11627
11858
  await this.writer.apply(commit.projectId, commit.id, input.chunk);
11859
+ session.changedObjects += input.chunk.effective.objectUpserts.length + input.chunk.effective.objectDeletes.length;
11860
+ session.changedLinks += input.chunk.effective.linkUpserts.length + input.chunk.effective.linkDeletes.length;
11628
11861
  this.sessions.commitChunkSequence(session, progress);
11629
11862
  }
11630
11863
  async finalize(input) {
@@ -11634,6 +11867,10 @@ class PgOntologyMaterializationStorage {
11634
11867
  for (const activation of input.finalization.sourceActivations) {
11635
11868
  await this.activateSource(session, activation);
11636
11869
  }
11870
+ if (session.changedObjects >= 1000)
11871
+ await this.sql`ANALYZE objects`;
11872
+ if (session.changedLinks >= 1000)
11873
+ await this.sql`ANALYZE links`;
11637
11874
  const record = await this.insertCommit(session.header, input);
11638
11875
  await this.sessions.release(session);
11639
11876
  return { commit: record };
@@ -11644,14 +11881,14 @@ class PgOntologyMaterializationStorage {
11644
11881
  async requireReplacement(session, input) {
11645
11882
  if (session.replacement) {
11646
11883
  if (session.replacement.sourceId !== input.source.projectionId || session.replacement.candidateMaterializationId !== input.candidateMaterializationId) {
11647
- throw new MaterializationConflictError5("source-materialization", "Materialization session already owns another replacement union.");
11884
+ throw new MaterializationConflictError6("source-materialization", "Materialization session already owns another replacement union.");
11648
11885
  }
11649
11886
  return session.replacement;
11650
11887
  }
11651
11888
  const projectId = session.header.commit.projectId;
11652
11889
  const candidate = await this.getSource(projectId, input.source.projectionId, input.candidateMaterializationId, true);
11653
11890
  if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
11654
- throw new MaterializationConflictError5("source-materialization", `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`);
11891
+ throw new MaterializationConflictError6("source-materialization", `Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`);
11655
11892
  }
11656
11893
  await assertProjectionExecution(this.sql, {
11657
11894
  projectId,
@@ -11661,12 +11898,13 @@ class PgOntologyMaterializationStorage {
11661
11898
  });
11662
11899
  const previous = await this.getActiveSource(projectId, input.source.projectionId, true);
11663
11900
  if (candidate.protocol !== "replacement" || previous && (previous.protocol !== candidate.protocol || previous.projection_kind !== candidate.projection_kind)) {
11664
- throw new MaterializationConflictError5("source-materialization", "Source replacement kind or protocol does not match its active materialization.");
11901
+ throw new MaterializationConflictError6("source-materialization", "Source replacement kind or protocol does not match its active materialization.");
11665
11902
  }
11666
11903
  session.replacement = {
11667
11904
  sourceId: input.source.projectionId,
11668
11905
  candidateMaterializationId: input.candidateMaterializationId,
11669
11906
  previousMaterializationId: previous?.materialization_id ?? null,
11907
+ incremental: candidate.base_materialization_id !== null,
11670
11908
  projectionKind: candidate.projection_kind,
11671
11909
  objectStreamStarted: false,
11672
11910
  objectStreamCompleted: false,
@@ -11686,7 +11924,7 @@ class PgOntologyMaterializationStorage {
11686
11924
  FOR UPDATE
11687
11925
  `;
11688
11926
  if (duplicateIdentity) {
11689
- throw new MaterializationConflictError5("idempotency", duplicateIdentity.id === header.commit.id ? `Ontology commit '${header.commit.id}' already exists.` : "Ontology idempotency key already exists.");
11927
+ throw new MaterializationConflictError6("idempotency", duplicateIdentity.id === header.commit.id ? `Ontology commit '${header.commit.id}' already exists.` : "Ontology idempotency key already exists.");
11690
11928
  }
11691
11929
  const origin = originColumns(header.commit.origin);
11692
11930
  if (origin.runId === null)
@@ -11709,7 +11947,7 @@ class PgOntologyMaterializationStorage {
11709
11947
  FOR UPDATE
11710
11948
  `;
11711
11949
  if (duplicateOrigin) {
11712
- throw new MaterializationConflictError5("run-correlation", "Ontology commit origin already has an authoritative commit.");
11950
+ throw new MaterializationConflictError6("run-correlation", "Ontology commit origin already has an authoritative commit.");
11713
11951
  }
11714
11952
  }
11715
11953
  async assertSources(expectedSources, projectId) {
@@ -11731,29 +11969,29 @@ class PgOntologyMaterializationStorage {
11731
11969
  }
11732
11970
  assertSourceRow(expected, active) {
11733
11971
  if ((active?.materialization_id ?? null) !== expected.activeMaterializationId || (active?.last_commit_id ?? null) !== expected.lastCommitId) {
11734
- throw new MaterializationConflictError5("projection-fence", `Source '${expected.source.projectionId}' changed.`);
11972
+ throw new MaterializationConflictError6("projection-fence", `Source '${expected.source.projectionId}' changed.`);
11735
11973
  }
11736
11974
  }
11737
11975
  assertObject(row, expected) {
11738
11976
  if (!expected.exists) {
11739
11977
  if (row) {
11740
- throw new MaterializationConflictError5("effective-state", `Expected object ${objectRefKey3(expected.ref)} to be absent.`);
11978
+ throw new MaterializationConflictError6("effective-state", `Expected object ${objectRefKey3(expected.ref)} to be absent.`);
11741
11979
  }
11742
11980
  return;
11743
11981
  }
11744
11982
  if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
11745
- throw new MaterializationConflictError5("effective-state", `Expected object ${objectRefKey3(expected.ref)} changed.`);
11983
+ throw new MaterializationConflictError6("effective-state", `Expected object ${objectRefKey3(expected.ref)} changed.`);
11746
11984
  }
11747
11985
  }
11748
11986
  assertLink(lastCommitId, expected) {
11749
11987
  if (!expected.exists) {
11750
11988
  if (lastCommitId !== undefined) {
11751
- throw new MaterializationConflictError5("effective-state", `Expected link ${linkRefKey3(expected.ref)} to be absent.`);
11989
+ throw new MaterializationConflictError6("effective-state", `Expected link ${linkRefKey3(expected.ref)} to be absent.`);
11752
11990
  }
11753
11991
  return;
11754
11992
  }
11755
11993
  if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
11756
- throw new MaterializationConflictError5("effective-state", `Expected link ${linkRefKey3(expected.ref)} changed.`);
11994
+ throw new MaterializationConflictError6("effective-state", `Expected link ${linkRefKey3(expected.ref)} changed.`);
11757
11995
  }
11758
11996
  }
11759
11997
  async assertFinalization(session, input) {
@@ -11762,7 +12000,8 @@ class PgOntologyMaterializationStorage {
11762
12000
  assertMaterializationFinalizationCorrelation(session, input);
11763
12001
  const laneCounts = await this.sessions.laneCounts(session);
11764
12002
  assertMaterializationLaneCompletion(session, laneCounts);
11765
- await this.assertFinalCardinality(session);
12003
+ if (laneCounts.cardinality > 0)
12004
+ await this.assertFinalCardinality(session);
11766
12005
  const eventCount = laneCounts.event;
11767
12006
  const [outbox] = await this.sql`
11768
12007
  SELECT COUNT(*) AS count, MIN(commit_ordinal) AS minimum,
@@ -11795,17 +12034,18 @@ class PgOntologyMaterializationStorage {
11795
12034
  }
11796
12035
  }
11797
12036
  async assertFinalCardinality(session) {
11798
- const [violation] = await this.sql`
11799
- WITH work AS (
11800
- SELECT payload->>'scopeSortKey' AS scope_sort_key,
11801
- payload->>'linkSortKey' AS link_sort_key,
12037
+ const table = `ontology_cardinality_${session.id.replaceAll("-", "")}`;
12038
+ await this.sql.unsafe(`CREATE TEMP TABLE ${table} ON COMMIT DROP AS
12039
+ SELECT sort_one AS scope_sort_key, sort_two AS link_sort_key,
11802
12040
  (payload->>'occupied')::boolean AS occupied,
11803
12041
  payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
11804
12042
  payload->'ref'->'source'->>'primaryId' AS source_id,
11805
12043
  payload->'ref'->>'linkId' AS link_id
11806
- FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
11807
- WHERE session_id = ${session.id} AND kind = 'cardinality'
11808
- AND payload->>'view' = 'effective'
12044
+ FROM ${PG_MATERIALIZATION_WORK_TABLE}
12045
+ WHERE session_id = $1 AND lane = 'cardinality' AND major_order = 0`, [session.id]);
12046
+ await this.sql.unsafe(`ANALYZE ${table}`);
12047
+ const [violation] = await this.sql.unsafe(`WITH work AS (
12048
+ SELECT * FROM ${table}
11809
12049
  ), duplicate AS (
11810
12050
  SELECT scope_sort_key FROM work WHERE occupied
11811
12051
  GROUP BY scope_sort_key HAVING COUNT(*) > 1
@@ -11815,10 +12055,10 @@ class PgOntologyMaterializationStorage {
11815
12055
  SELECT scope_sort_key, link_sort_key FROM work WHERE occupied
11816
12056
  ), actual AS (
11817
12057
  SELECT scopes.scope_sort_key,
11818
- ${this.sql.unsafe(linkSortExpression("links"))} AS link_sort_key
12058
+ ${linkSortExpression("links")} AS link_sort_key
11819
12059
  FROM scopes
11820
12060
  JOIN links USING (source_type_id, source_id, link_id)
11821
- WHERE links.project_id = ${session.header.commit.projectId}
12061
+ WHERE links.project_id = $1
11822
12062
  ), differences AS (
11823
12063
  (SELECT * FROM expected EXCEPT SELECT * FROM actual)
11824
12064
  UNION ALL
@@ -11827,8 +12067,8 @@ class PgOntologyMaterializationStorage {
11827
12067
  SELECT 'duplicate'::text AS reason FROM duplicate
11828
12068
  UNION ALL
11829
12069
  SELECT 'mismatch'::text AS reason FROM differences
11830
- LIMIT 1
11831
- `;
12070
+ LIMIT 1`, [session.header.commit.projectId]);
12071
+ await this.sql.unsafe(`DROP TABLE ${table}`);
11832
12072
  if (violation?.reason === "duplicate") {
11833
12073
  invalidCorrelation2("Materialization cardinality work violates cardinality-one.");
11834
12074
  }
@@ -11924,7 +12164,7 @@ class PgOntologyMaterializationStorage {
11924
12164
  assertSourceActivationCorrelation(session, activation);
11925
12165
  const candidate = await this.getSource(commit.projectId, activation.source.projectionId, activation.materializationId, true);
11926
12166
  if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
11927
- throw new MaterializationConflictError5("source-materialization", "Source activation candidate is missing or is not ready.");
12167
+ throw new MaterializationConflictError6("source-materialization", "Source activation candidate is missing or is not ready.");
11928
12168
  }
11929
12169
  await assertProjectionExecution(this.sql, {
11930
12170
  projectId: commit.projectId,
@@ -11937,6 +12177,7 @@ class PgOntologyMaterializationStorage {
11937
12177
  }
11938
12178
  const previous = await this.getActiveSource(commit.projectId, activation.source.projectionId, true);
11939
12179
  this.assertSourceRow(activation.expected, previous);
12180
+ await activateSourceRoots(this.sql, commit.projectId, candidate, activation);
11940
12181
  if (previous) {
11941
12182
  assertPinnedDatasetWatermark({
11942
12183
  datasetId: previous.dataset_id,
@@ -11958,7 +12199,7 @@ class PgOntologyMaterializationStorage {
11958
12199
  RETURNING materialization_id
11959
12200
  `;
11960
12201
  if (superseded.length !== 1) {
11961
- throw new MaterializationConflictError5("projection-fence", `Source '${activation.source.projectionId}' changed.`);
12202
+ throw new MaterializationConflictError6("projection-fence", `Source '${activation.source.projectionId}' changed.`);
11962
12203
  }
11963
12204
  }
11964
12205
  let activated;
@@ -11977,12 +12218,12 @@ class PgOntologyMaterializationStorage {
11977
12218
  `;
11978
12219
  } catch (error) {
11979
12220
  if (isUniqueViolation(error)) {
11980
- throw new MaterializationConflictError5("projection-fence", `Source '${activation.source.projectionId}' changed.`);
12221
+ throw new MaterializationConflictError6("projection-fence", `Source '${activation.source.projectionId}' changed.`);
11981
12222
  }
11982
12223
  throw error;
11983
12224
  }
11984
12225
  if (activated.length !== 1) {
11985
- throw new MaterializationConflictError5("source-materialization", "Source activation candidate changed.");
12226
+ throw new MaterializationConflictError6("source-materialization", "Source activation candidate changed.");
11986
12227
  }
11987
12228
  }
11988
12229
  async insertCommit(header, input) {
@@ -12009,7 +12250,7 @@ class PgOntologyMaterializationStorage {
12009
12250
  `;
12010
12251
  if (!rows[0]) {
12011
12252
  await this.assertCommitAbsent(header);
12012
- throw new MaterializationConflictError5("idempotency", "Ontology commit identity already exists.");
12253
+ throw new MaterializationConflictError6("idempotency", "Ontology commit identity already exists.");
12013
12254
  }
12014
12255
  return commitRecord(rows[0]);
12015
12256
  }
@@ -12057,8 +12298,8 @@ function databaseCount(value) {
12057
12298
  // src/ontology-storage/outbox.ts
12058
12299
  import { serializeSixbFailure as serializeSixbFailure2 } from "@sixb/core/internal/errors";
12059
12300
  import {
12060
- MaterializationConflictError as MaterializationConflictError6,
12061
- MaterializationValidationError
12301
+ MaterializationConflictError as MaterializationConflictError7,
12302
+ MaterializationValidationError as MaterializationValidationError2
12062
12303
  } from "@sixb/core/internal/materialization";
12063
12304
  import { ONTOLOGY_OUTBOX_FAILURE_CODES as ONTOLOGY_OUTBOX_FAILURE_CODES2 } from "@sixb/core/storage";
12064
12305
  class PgOntologyOutboxStorage {
@@ -12074,7 +12315,7 @@ class PgOntologyOutboxStorage {
12074
12315
  const leaseExpiry = assertTimestamp(input.leaseExpiresAt, "Ontology outbox lease expiry");
12075
12316
  assertNonblank(input.leaseId, "Ontology outbox lease id");
12076
12317
  if (leaseExpiry <= now) {
12077
- throw new MaterializationValidationError("Ontology outbox lease expiry must be later than the claim time.");
12318
+ throw new MaterializationValidationError2("Ontology outbox lease expiry must be later than the claim time.");
12078
12319
  }
12079
12320
  const rows = await sql`
12080
12321
  WITH candidates AS (
@@ -12104,7 +12345,7 @@ class PgOntologyOutboxStorage {
12104
12345
  return rows.map((row) => {
12105
12346
  const record = outboxRecord(row);
12106
12347
  if (record.leaseId === null || record.leaseExpiresAt === null) {
12107
- throw new MaterializationConflictError6("outbox-lease", "Ontology outbox claim returned an unpaired lease.");
12348
+ throw new MaterializationConflictError7("outbox-lease", "Ontology outbox claim returned an unpaired lease.");
12108
12349
  }
12109
12350
  return { ...record, leaseId: record.leaseId, leaseExpiresAt: record.leaseExpiresAt };
12110
12351
  });
@@ -12225,7 +12466,7 @@ function validateLeaseIds(ids) {
12225
12466
  for (const id of ids) {
12226
12467
  assertNonblank(id, "Ontology outbox event id");
12227
12468
  if (seen.has(id)) {
12228
- throw new MaterializationValidationError(`Ontology outbox lease batch repeats event '${id}'.`);
12469
+ throw new MaterializationValidationError2(`Ontology outbox lease batch repeats event '${id}'.`);
12229
12470
  }
12230
12471
  seen.add(id);
12231
12472
  }
@@ -12236,11 +12477,11 @@ function assertLeaseInput(input) {
12236
12477
  assertNonblank(input.leaseId, "Ontology outbox lease id");
12237
12478
  }
12238
12479
  function leaseConflict() {
12239
- return new MaterializationConflictError6("outbox-lease", "Ontology outbox lease does not match.");
12480
+ return new MaterializationConflictError7("outbox-lease", "Ontology outbox lease does not match.");
12240
12481
  }
12241
12482
 
12242
12483
  // src/ontology-storage/sources.ts
12243
- import { MaterializationValidationError as MaterializationValidationError2 } from "@sixb/core/internal/materialization";
12484
+ import { MaterializationValidationError as MaterializationValidationError3 } from "@sixb/core/internal/materialization";
12244
12485
  import {
12245
12486
  assertSourceBeginInput as assertBeginInput,
12246
12487
  assertSourceExecutionIdentity as assertExecutionIdentity,
@@ -12254,8 +12495,61 @@ import {
12254
12495
  sourceMaterializationIdentity,
12255
12496
  sourceStageRow,
12256
12497
  sourceStageRows,
12257
- utf8SortKey
12498
+ utf8SortKey as utf8SortKey2
12258
12499
  } from "@sixb/core/internal/ontology-storage-provider";
12500
+
12501
+ // src/ontology-storage/source-cleanup.ts
12502
+ async function cleanupSourceVersions(sql, input) {
12503
+ let remaining = input.limit;
12504
+ const removedRows = await sql`
12505
+ WITH selected AS (
12506
+ SELECT rows.ctid FROM ontology_source_roots AS roots
12507
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
12508
+ JOIN ontology_source_rows AS rows
12509
+ ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12510
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
12511
+ WHERE roots.project_id = ${input.projectId} AND NOT roots.active AND roots.retired_at < ${input.terminalBefore}
12512
+ AND sources.status IN ('superseded', 'abandoned')
12513
+ ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
12514
+ LIMIT ${remaining} FOR UPDATE OF rows SKIP LOCKED
12515
+ ) DELETE FROM ontology_source_rows AS rows USING selected WHERE rows.ctid = selected.ctid RETURNING 1
12516
+ `;
12517
+ let rowsDeleted = removedRows.length;
12518
+ remaining -= rowsDeleted;
12519
+ if (remaining > 0) {
12520
+ const removed = await sql`
12521
+ WITH selected AS (
12522
+ SELECT roots.ctid FROM ontology_source_roots AS roots
12523
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
12524
+ WHERE roots.project_id = ${input.projectId} AND NOT roots.active AND roots.retired_at < ${input.terminalBefore}
12525
+ AND sources.status IN ('superseded', 'abandoned')
12526
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
12527
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
12528
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
12529
+ ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key
12530
+ LIMIT ${remaining} FOR UPDATE OF roots SKIP LOCKED
12531
+ ) DELETE FROM ontology_source_roots AS roots USING selected WHERE roots.ctid = selected.ctid RETURNING 1
12532
+ `;
12533
+ rowsDeleted += removed.length;
12534
+ remaining -= removed.length;
12535
+ }
12536
+ const removedManifests = remaining === 0 ? [] : await sql`
12537
+ WITH selected AS (
12538
+ SELECT sources.ctid FROM ontology_sources AS sources
12539
+ WHERE project_id = ${input.projectId} AND status IN ('superseded', 'abandoned') AND terminal_at < ${input.terminalBefore}
12540
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
12541
+ WHERE roots.project_id = sources.project_id AND roots.source_id = sources.source_id
12542
+ AND roots.materialization_id = sources.materialization_id)
12543
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
12544
+ WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
12545
+ AND rows.materialization_id = sources.materialization_id)
12546
+ ORDER BY terminal_at, source_id, materialization_id LIMIT ${remaining} FOR UPDATE SKIP LOCKED
12547
+ ) DELETE FROM ontology_sources AS sources USING selected WHERE sources.ctid = selected.ctid RETURNING 1
12548
+ `;
12549
+ return { rowsDeleted, materializationsDeleted: removedManifests.length };
12550
+ }
12551
+
12552
+ // src/ontology-storage/sources.ts
12259
12553
  class PgOntologySourceStorage {
12260
12554
  runRootOperation;
12261
12555
  constructor(runRootOperation) {
@@ -12286,14 +12580,15 @@ class PgOntologySourceStorage {
12286
12580
  dataset_id, dataset_version_id, dataset_version_created_at,
12287
12581
  projection_revision, ownership_hash, ontology_revision,
12288
12582
  root_count, assertion_count, created_at, ready_at, activated_at,
12289
- terminal_at, last_commit_id, updated_at
12583
+ terminal_at, last_commit_id, updated_at, base_materialization_id, base_commit_id
12290
12584
  ) VALUES (
12291
12585
  ${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
12292
12586
  ${input.execution.projectionRunId}, ${input.projectionKind}, 'replacement',
12293
12587
  'staging', ${input.execution.executionToken}, ${input.datasetVersion.datasetId},
12294
12588
  ${input.datasetVersion.versionId}, ${input.datasetVersion.createdAt},
12295
12589
  ${input.projectionRevision}, ${input.ownershipHash}, ${input.ontologyRevision},
12296
- NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt}
12590
+ NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt},
12591
+ ${input.base?.materializationId ?? null}, ${input.base?.lastCommitId ?? null}
12297
12592
  )
12298
12593
  ON CONFLICT DO NOTHING
12299
12594
  RETURNING *
@@ -12322,6 +12617,7 @@ class PgOntologySourceStorage {
12322
12617
  const rows = sourceStageRows(manifest.projection_kind, input.rows);
12323
12618
  const existing = await this.findStageRows(sql, input, rows);
12324
12619
  const { pending, unchanged } = reconcileSourceStageRows(rows, existing);
12620
+ await stageSourceRoots(sql, manifest, input);
12325
12621
  await this.insertStageRows(sql, input, pending);
12326
12622
  return { inserted: pending.length, unchanged };
12327
12623
  });
@@ -12345,7 +12641,10 @@ class PgOntologySourceStorage {
12345
12641
  throw sourceConflict(`Source materialization '${input.materializationId}' cannot transition from '${manifest.status}' to 'ready'.`);
12346
12642
  }
12347
12643
  if (input.readyAt < toIsoString2(manifest.created_at)) {
12348
- throw new MaterializationValidationError2("Source readyAt cannot precede source createdAt.");
12644
+ throw new MaterializationValidationError3("Source readyAt cannot precede source createdAt.");
12645
+ }
12646
+ if (input.assertionCount >= 1000 || input.rootCount >= 1000) {
12647
+ await sql`ANALYZE ontology_source_rows, ontology_source_roots`;
12349
12648
  }
12350
12649
  await this.assertReadyState(sql, manifest, input.rootCount, input.assertionCount);
12351
12650
  const rows = await sql`
@@ -12392,61 +12691,7 @@ class PgOntologySourceStorage {
12392
12691
  assertNonblank(input.projectId, "Terminal source cleanup project id");
12393
12692
  assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true);
12394
12693
  assertPositiveInteger(input.limit, "Terminal source cleanup limit");
12395
- const manifests = await sql`
12396
- SELECT * FROM ontology_sources
12397
- WHERE project_id = ${input.projectId}
12398
- AND status IN ('superseded', 'abandoned')
12399
- AND terminal_at < ${input.terminalBefore}
12400
- ORDER BY terminal_at, source_id, materialization_id
12401
- LIMIT ${input.limit}
12402
- FOR UPDATE SKIP LOCKED
12403
- `;
12404
- let remaining = input.limit;
12405
- let rowsDeleted = 0;
12406
- let materializationsDeleted = 0;
12407
- for (const manifest of manifests) {
12408
- if (remaining === 0)
12409
- break;
12410
- const deletedRows = await sql`
12411
- WITH selected AS (
12412
- SELECT rows.ctid
12413
- FROM ontology_source_rows AS rows
12414
- WHERE rows.project_id = ${manifest.project_id}
12415
- AND rows.source_id = ${manifest.source_id}
12416
- AND rows.materialization_id = ${manifest.materialization_id}
12417
- ORDER BY rows.entity_sort_key
12418
- LIMIT ${remaining}
12419
- FOR UPDATE SKIP LOCKED
12420
- )
12421
- DELETE FROM ontology_source_rows AS rows
12422
- USING selected
12423
- WHERE rows.ctid = selected.ctid
12424
- RETURNING rows.entity_sort_key
12425
- `;
12426
- rowsDeleted += deletedRows.length;
12427
- remaining -= deletedRows.length;
12428
- if (remaining === 0)
12429
- break;
12430
- const removed = await sql`
12431
- DELETE FROM ontology_sources AS sources
12432
- WHERE sources.project_id = ${manifest.project_id}
12433
- AND sources.source_id = ${manifest.source_id}
12434
- AND sources.materialization_id = ${manifest.materialization_id}
12435
- AND sources.status IN ('superseded', 'abandoned')
12436
- AND sources.terminal_at = ${manifest.terminal_at}
12437
- AND sources.terminal_at < ${input.terminalBefore}
12438
- AND NOT EXISTS (
12439
- SELECT 1 FROM ontology_source_rows AS rows
12440
- WHERE rows.project_id = sources.project_id
12441
- AND rows.source_id = sources.source_id
12442
- AND rows.materialization_id = sources.materialization_id
12443
- )
12444
- RETURNING sources.materialization_id
12445
- `;
12446
- materializationsDeleted += removed.length;
12447
- remaining -= removed.length;
12448
- }
12449
- return { rowsDeleted, materializationsDeleted };
12694
+ return cleanupSourceVersions(sql, input);
12450
12695
  });
12451
12696
  }
12452
12697
  async summarizeTerminal(input) {
@@ -12494,7 +12739,7 @@ class PgOntologySourceStorage {
12494
12739
  }
12495
12740
  async transitionToAbandoned(sql, manifest, abandonedAt) {
12496
12741
  if (abandonedAt < toIsoString2(manifest.created_at) || manifest.ready_at !== null && abandonedAt < toIsoString2(manifest.ready_at)) {
12497
- throw new MaterializationValidationError2("Source abandonedAt cannot precede source creation or readiness.");
12742
+ throw new MaterializationValidationError3("Source abandonedAt cannot precede source creation or readiness.");
12498
12743
  }
12499
12744
  const rows = await sql`
12500
12745
  UPDATE ontology_sources
@@ -12509,14 +12754,19 @@ class PgOntologySourceStorage {
12509
12754
  if (!rows[0]) {
12510
12755
  throw sourceConflict(`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`);
12511
12756
  }
12757
+ await sql`UPDATE ontology_source_roots SET retired_at = ${abandonedAt}
12758
+ WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
12759
+ AND materialization_id = ${manifest.materialization_id} AND NOT active`;
12512
12760
  return sourceRecord(rows[0]);
12513
12761
  }
12514
12762
  async assertReadyState(sql, manifest, rootCount, assertionCount) {
12515
12763
  const [counts] = await sql`
12516
- SELECT COUNT(*) AS assertions, COUNT(DISTINCT root_key) AS roots,
12764
+ SELECT (SELECT COUNT(*) FROM ontology_source_rows
12765
+ WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
12766
+ AND materialization_id = ${manifest.materialization_id}) AS assertions, COUNT(*) AS roots,
12517
12767
  COUNT(DISTINCT staging_ordinal) AS ordinals,
12518
12768
  MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
12519
- FROM ontology_source_rows
12769
+ FROM ontology_source_roots
12520
12770
  WHERE project_id = ${manifest.project_id}
12521
12771
  AND source_id = ${manifest.source_id}
12522
12772
  AND materialization_id = ${manifest.materialization_id}
@@ -12527,8 +12777,9 @@ class PgOntologySourceStorage {
12527
12777
  const minimum = counts?.min_ordinal === null ? null : Number(counts?.min_ordinal);
12528
12778
  const maximum = counts?.max_ordinal === null ? null : Number(counts?.max_ordinal);
12529
12779
  if (assertions !== assertionCount || roots !== rootCount || ordinals !== rootCount || rootCount > 0 && (minimum !== 0 || maximum !== rootCount - 1)) {
12530
- throw new MaterializationValidationError2("Source ready counts do not match the staged roots and assertions.");
12780
+ throw new MaterializationValidationError3("Source ready counts do not match the staged roots and assertions.");
12531
12781
  }
12782
+ await assertSourceRootCoverage(sql, manifest);
12532
12783
  const [invalid] = manifest.projection_kind === "link" ? await sql`
12533
12784
  SELECT root_key
12534
12785
  FROM ontology_source_rows
@@ -12562,15 +12813,15 @@ class PgOntologySourceStorage {
12562
12813
  LIMIT 1
12563
12814
  `;
12564
12815
  if (invalid) {
12565
- 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.");
12816
+ 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.");
12566
12817
  }
12567
12818
  }
12568
12819
  async findStageRows(sql, input, rows) {
12569
12820
  if (rows.length === 0)
12570
12821
  return [];
12571
- const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey(row.rootKey)))];
12822
+ const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.rootKey)))];
12572
12823
  const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))];
12573
- const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey(row.entityKey)))];
12824
+ const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey2(row.entityKey)))];
12574
12825
  const existing = await sql`
12575
12826
  SELECT * FROM ontology_source_rows
12576
12827
  WHERE project_id = ${input.projectId}
@@ -12593,10 +12844,10 @@ class PgOntologySourceStorage {
12593
12844
  return {
12594
12845
  entityKind: row.assertion.kind,
12595
12846
  entityKey: JSON.parse(entityKey),
12596
- entitySortKey: utf8SortKey(entityKey),
12847
+ entitySortKey: utf8SortKey2(entityKey),
12597
12848
  rootKind: row.root.kind,
12598
12849
  rootKey: JSON.parse(rootKey),
12599
- rootSortKey: utf8SortKey(rootKey),
12850
+ rootSortKey: utf8SortKey2(rootKey),
12600
12851
  stagingOrdinal: row.stagingOrdinal,
12601
12852
  root: row.root,
12602
12853
  assertion: row.assertion,
@@ -15256,7 +15507,7 @@ class PgProjectionRunStorage {
15256
15507
  input_exhausted = ${input.fixedBatchSize === undefined ? null : false},
15257
15508
  missing_target_object_type_id = ${null}, missing_target_object_id = ${null},
15258
15509
  missing_target_batch_ordinal = ${null}, missing_target_first_seen_at = ${null},
15259
- source_rows_read = ${0}, source_rows_skipped = ${0}, error = ${null}
15510
+ source_rows_read = ${0}, source_rows_skipped = ${0}, source_changes_read = ${null}, error = ${null}
15260
15511
  WHERE project_id = ${input.projectId} AND id = ${input.id}
15261
15512
  AND status = ${"failed"} AND error->>'code' = ${"queue.enqueue_failed"}
15262
15513
  RETURNING *
@@ -15367,7 +15618,8 @@ class PgProjectionRunStorage {
15367
15618
  UPDATE projection_runs
15368
15619
  SET
15369
15620
  source_rows_read = ${progress.sourceRowsRead},
15370
- source_rows_skipped = ${progress.sourceRowsSkipped}
15621
+ source_rows_skipped = ${progress.sourceRowsSkipped},
15622
+ source_changes_read = ${progress.sourceChangesRead ?? null}
15371
15623
  WHERE project_id = ${input.projectId}
15372
15624
  AND id = ${input.id}
15373
15625
  AND status = ${"running"}
@@ -15392,6 +15644,7 @@ class PgProjectionRunStorage {
15392
15644
  execution_token = ${null},
15393
15645
  source_rows_read = ${plan.progress.sourceRowsRead},
15394
15646
  source_rows_skipped = ${plan.progress.sourceRowsSkipped},
15647
+ source_changes_read = ${plan.progress.sourceChangesRead ?? null},
15395
15648
  input_exhausted = ${plan.inputExhausted ?? existingRow.input_exhausted},
15396
15649
  error = ${plan.error === undefined ? null : serializeSixbFailure4(plan.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
15397
15650
  WHERE project_id = ${input.projectId}
@@ -15634,7 +15887,8 @@ function restoreProjectionRunRow(row) {
15634
15887
  missingTargetFirstSeenAt: row.missing_target_first_seen_at ? new Date(row.missing_target_first_seen_at) : undefined,
15635
15888
  progress: {
15636
15889
  sourceRowsRead: databaseSafeInteger2(row.source_rows_read, "sourceRowsRead"),
15637
- sourceRowsSkipped: databaseSafeInteger2(row.source_rows_skipped, "sourceRowsSkipped")
15890
+ sourceRowsSkipped: databaseSafeInteger2(row.source_rows_skipped, "sourceRowsSkipped"),
15891
+ ...row.source_changes_read == null ? {} : { sourceChangesRead: databaseSafeInteger2(row.source_changes_read, "sourceChangesRead") }
15638
15892
  },
15639
15893
  error: row.error ?? undefined
15640
15894
  };
@@ -17866,4 +18120,4 @@ export {
17866
18120
  migratePostgresStorage
17867
18121
  };
17868
18122
 
17869
- //# debugId=E76835E14E96BFEC64756E2164756E21
18123
+ //# debugId=0CAAAAC02FB5CFD864756E2164756E21