@sixb/pg 0.1.8 → 0.1.10

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 (53) hide show
  1. package/dist/agents/rows.d.ts +0 -1
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts.map +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +562 -299
  6. package/dist/index.js.map +24 -22
  7. package/dist/migrations.d.ts.map +1 -1
  8. package/dist/ontology-storage/materialization-session.d.ts +3 -0
  9. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  10. package/dist/ontology-storage/materialization-state.d.ts +2 -1
  11. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  12. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  13. package/dist/ontology-storage/shared.d.ts +2 -0
  14. package/dist/ontology-storage/shared.d.ts.map +1 -1
  15. package/dist/ontology-storage/source-cleanup.d.ts +4 -0
  16. package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
  17. package/dist/ontology-storage/source-roots.d.ts +24 -0
  18. package/dist/ontology-storage/source-roots.d.ts.map +1 -0
  19. package/dist/ontology-storage/sources.d.ts.map +1 -1
  20. package/dist/pg-action-run-storage.d.ts.map +1 -1
  21. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  22. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  23. package/dist/pg-execution-storage.d.ts.map +1 -1
  24. package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
  25. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  26. package/dist/pg-sync-run-storage.d.ts.map +1 -1
  27. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  28. package/package.json +4 -4
  29. package/src/agents/rows.ts +0 -5
  30. package/src/agents/runs.ts +1 -6
  31. package/src/ai-cost-groups.ts +3 -0
  32. package/src/connector-connection-storage.ts +3 -3
  33. package/src/index.ts +4 -2
  34. package/src/migrations/037-execution-requester-groups.sql +21 -0
  35. package/src/migrations/038-outbox-publication-order.sql +5 -0
  36. package/src/migrations/039-projection-source-roots.sql +46 -0
  37. package/src/migrations/040-connector-optional-pkce.sql +3 -0
  38. package/src/migrations.ts +18 -0
  39. package/src/ontology-storage/materialization-session.ts +24 -8
  40. package/src/ontology-storage/materialization-state.ts +65 -86
  41. package/src/ontology-storage/materializations.ts +39 -13
  42. package/src/ontology-storage/shared.ts +10 -0
  43. package/src/ontology-storage/source-cleanup.ts +61 -0
  44. package/src/ontology-storage/source-roots.ts +251 -0
  45. package/src/ontology-storage/sources.ts +21 -57
  46. package/src/pg-action-run-storage.ts +6 -7
  47. package/src/pg-ai-cost-storage.ts +11 -2
  48. package/src/pg-ai-usage-storage.ts +1 -2
  49. package/src/pg-execution-storage.ts +6 -4
  50. package/src/pg-pipeline-run-storage.ts +2 -5
  51. package/src/pg-projection-run-storage.ts +8 -2
  52. package/src/pg-sync-run-storage.ts +2 -5
  53. package/src/pg-workflow-run-storage.ts +0 -5
@@ -46,6 +46,7 @@ import {
46
46
  type PgStoredOverrideRow,
47
47
  toIsoString,
48
48
  } from "./shared"
49
+ import { activeSourceRows, replacementAssertionRows, replacementSourceRows } from "./source-roots"
49
50
 
50
51
  export const PG_MATERIALIZATION_WORK_TABLE = "ontology_materialization_work"
51
52
  export const PG_REPLACEMENT_WORK_TABLE = "ontology_replacement_work"
@@ -141,6 +142,7 @@ interface ReplacementSources {
141
142
  }
142
143
 
143
144
  interface ReplacementIdentityInput {
145
+ readonly incremental?: boolean
144
146
  readonly sessionId: string
145
147
  readonly sourceId: string
146
148
  readonly candidateMaterializationId: string
@@ -186,22 +188,11 @@ export class PgMaterializationStateReader {
186
188
  JOIN requested USING (object_type_id, primary_id)
187
189
  WHERE objects.project_id = ${this.projectId}
188
190
  `,
189
- this.sql<PgOntologySourceAssertionRow[]>`
190
- WITH requested AS (
191
- SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
192
- AS requested_values(object_type_id TEXT, primary_id TEXT)
193
- )
194
- SELECT rows.*
195
- FROM ontology_source_rows AS rows
196
- JOIN requested USING (object_type_id, primary_id)
197
- JOIN ontology_sources AS sources
198
- ON sources.project_id = rows.project_id
199
- AND sources.source_id = rows.source_id
200
- AND sources.materialization_id = rows.materialization_id
201
- WHERE rows.project_id = ${this.projectId}
202
- AND rows.entity_kind = 'object'
203
- AND sources.status = 'active'
204
- `,
191
+ activeSourceRows(
192
+ this.sql,
193
+ this.projectId,
194
+ refs.map((ref) => ({ kind: "object", ref }))
195
+ ),
205
196
  this.sql<ObjectOverrideRow[]>`
206
197
  WITH requested AS (
207
198
  SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter})
@@ -291,26 +282,11 @@ export class PgMaterializationStateReader {
291
282
  JOIN requested USING (source_type_id, source_id, link_id, target_type_id, target_id)
292
283
  WHERE links.project_id = ${this.projectId}
293
284
  `,
294
- this.sql<PgOntologySourceAssertionRow[]>`
295
- WITH requested AS (
296
- SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
297
- source_type_id TEXT, source_primary_id TEXT, link_id TEXT,
298
- target_type_id TEXT, target_primary_id TEXT
299
- )
300
- )
301
- SELECT rows.*
302
- FROM ontology_source_rows AS rows
303
- JOIN requested USING (
304
- source_type_id, source_primary_id, link_id, target_type_id, target_primary_id
305
- )
306
- JOIN ontology_sources AS sources
307
- ON sources.project_id = rows.project_id
308
- AND sources.source_id = rows.source_id
309
- AND sources.materialization_id = rows.materialization_id
310
- WHERE rows.project_id = ${this.projectId}
311
- AND rows.entity_kind = 'link'
312
- AND sources.status = 'active'
313
- `,
285
+ activeSourceRows(
286
+ this.sql,
287
+ this.projectId,
288
+ refs.map((ref) => ({ kind: "link", ref }))
289
+ ),
314
290
  this.sql<LinkOverrideRow[]>`
315
291
  WITH requested AS (
316
292
  SELECT DISTINCT * FROM jsonb_to_recordset(${requestedParameter}) AS requested_values(
@@ -440,13 +416,14 @@ export class PgMaterializationStateReader {
440
416
  ON requested.source_type_id = rows.source_type_id
441
417
  AND requested.source_id = rows.source_primary_id
442
418
  AND requested.link_id = rows.link_id
443
- JOIN ontology_sources AS sources
419
+ JOIN ontology_source_roots AS sources
444
420
  ON sources.project_id = rows.project_id
445
421
  AND sources.source_id = rows.source_id
446
422
  AND sources.materialization_id = rows.materialization_id
423
+ AND sources.root_sort_key = rows.root_sort_key
447
424
  WHERE rows.project_id = ${this.projectId}
448
425
  AND rows.entity_kind = 'link'
449
- AND sources.status = 'active'
426
+ AND sources.active
450
427
  `,
451
428
  this.sql<LinkSlotOverrideRow[]>`
452
429
  WITH requested AS (
@@ -605,30 +582,32 @@ export class PgMaterializationStateReader {
605
582
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
606
583
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
607
584
  FROM ontology_source_rows AS rows
608
- JOIN ontology_sources AS sources
585
+ JOIN ontology_source_roots AS sources
609
586
  ON sources.project_id = rows.project_id
610
587
  AND sources.source_id = rows.source_id
611
588
  AND sources.materialization_id = rows.materialization_id
589
+ AND sources.root_sort_key = rows.root_sort_key
612
590
  JOIN requested
613
591
  ON requested.object_type_id = rows.source_type_id
614
592
  AND requested.primary_id = rows.source_primary_id
615
593
  WHERE rows.project_id = ${this.projectId}
616
594
  AND rows.entity_kind = 'link'
617
- AND sources.status = 'active'
595
+ AND sources.active
618
596
  UNION
619
597
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
620
598
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
621
599
  FROM ontology_source_rows AS rows
622
- JOIN ontology_sources AS sources
600
+ JOIN ontology_source_roots AS sources
623
601
  ON sources.project_id = rows.project_id
624
602
  AND sources.source_id = rows.source_id
625
603
  AND sources.materialization_id = rows.materialization_id
604
+ AND sources.root_sort_key = rows.root_sort_key
626
605
  JOIN requested
627
606
  ON requested.object_type_id = rows.target_type_id
628
607
  AND requested.primary_id = rows.target_primary_id
629
608
  WHERE rows.project_id = ${this.projectId}
630
609
  AND rows.entity_kind = 'link'
631
- AND sources.status = 'active'
610
+ AND sources.active
632
611
  ), selected AS (
633
612
  SELECT DISTINCT links.*,
634
613
  ${this.sql.unsafe(linkSortExpression("links"))} AS sort_key
@@ -649,13 +628,19 @@ export class PgMaterializationStateReader {
649
628
  input: ReplacementIdentityInput
650
629
  ): AsyncIterable<ReplacementIdentity[]> {
651
630
  await this.prepareReplacementIdentities(input)
631
+ // Temporary tables are not auto-analyzed. Refresh after each identity population is
632
+ // prepared so the planner can choose the ordered index for its bounded pages.
633
+ await this.sql`ANALYZE ${this.sql(PG_REPLACEMENT_WORK_TABLE)}`
652
634
  let cursor: string | null = null
653
635
  while (true) {
636
+ // Keep the next-page bound indexable in generic prepared plans. A nullable OR
637
+ // makes PostgreSQL revisit the preceding pages before applying the cursor filter.
638
+ const after = cursor === null ? this.sql`` : this.sql`AND sort_key > ${cursor}`
654
639
  const rows: ReplacementWorkRow[] = await this.sql<ReplacementWorkRow[]>`
655
640
  SELECT entity_kind, identity_key, sort_key, diff_required
656
641
  FROM ${this.sql(PG_REPLACEMENT_WORK_TABLE)}
657
642
  WHERE session_id = ${input.sessionId} AND entity_kind = ${input.kind}
658
- AND (${cursor}::text IS NULL OR sort_key > ${cursor})
643
+ ${after}
659
644
  ORDER BY sort_key
660
645
  LIMIT ${input.pageRows}
661
646
  `
@@ -693,14 +678,16 @@ export class PgMaterializationStateReader {
693
678
  sourceId: string,
694
679
  candidateMaterializationId: string,
695
680
  materializationIds: readonly string[],
696
- identities: readonly Extract<ReplacementIdentity, { readonly kind: "link" }>[]
681
+ identities: readonly Extract<ReplacementIdentity, { readonly kind: "link" }>[],
682
+ incremental = false
697
683
  ): Promise<readonly SourceReplacementLinkState[]> {
698
684
  const refs = identities.map((identity) => identity.ref)
699
685
  const base = await this.linkStates(refs)
700
686
  const replacements = await this.replacementSources(
701
687
  sourceId,
702
688
  materializationIds,
703
- refs.map((ref) => ({ kind: "link" as const, ref }))
689
+ refs.map((ref) => ({ kind: "link" as const, ref })),
690
+ incremental
704
691
  )
705
692
  const candidate = replacements.byMaterialization.get(candidateMaterializationId)
706
693
  return base.map((state, index) => {
@@ -807,29 +794,20 @@ export class PgMaterializationStateReader {
807
794
  private async replacementSources(
808
795
  sourceId: string,
809
796
  materializationIds: readonly string[],
810
- refs: readonly ProjectionEntityRef[]
797
+ refs: readonly ProjectionEntityRef[],
798
+ incremental = false
811
799
  ): Promise<ReplacementSources> {
812
800
  if (materializationIds.length === 0 || refs.length === 0) {
813
801
  return { owned: new Set(), byMaterialization: new Map() }
814
802
  }
815
- const keys = refs.map((ref) => ({
816
- entity_kind: ref.kind,
817
- entity_key: JSON.parse(projectionEntityKey(ref)) as unknown,
818
- }))
819
- const rows = await this.sql<PgOntologySourceAssertionRow[]>`
820
- WITH requested AS (
821
- SELECT *
822
- FROM jsonb_to_recordset(${jsonParameter(this.sql, keys)})
823
- AS requested_values(entity_kind TEXT, entity_key JSONB)
824
- )
825
- SELECT rows.* FROM ontology_source_rows AS rows
826
- JOIN requested USING (entity_kind, entity_key)
827
- WHERE rows.project_id = ${this.projectId}
828
- AND rows.source_id = ${sourceId}
829
- AND rows.materialization_id = ANY(
830
- ${this.sql.array([...new Set(materializationIds)])}::text[]
831
- )
832
- `
803
+ const rows = await replacementAssertionRows(this.sql, {
804
+ projectId: this.projectId,
805
+ sourceId,
806
+ materializationId: materializationIds[0]!,
807
+ incremental,
808
+ includePrevious: materializationIds.length > 1,
809
+ refs,
810
+ })
833
811
  const owned = new Set<string>()
834
812
  const byMaterialization = new Map<string, Map<string, StoredSourceAssertion>>()
835
813
  for (const row of rows) {
@@ -852,20 +830,19 @@ export class PgMaterializationStateReader {
852
830
  }
853
831
 
854
832
  private async prepareReplacementObjects(input: ReplacementIdentityInput): Promise<void> {
855
- const materializationIds = [
856
- input.candidateMaterializationId,
857
- ...(input.previousMaterializationId ? [input.previousMaterializationId] : []),
858
- ]
833
+ const rows = replacementSourceRows(this.sql, {
834
+ projectId: this.projectId,
835
+ sourceId: input.sourceId,
836
+ materializationId: input.candidateMaterializationId,
837
+ incremental: input.incremental ?? false,
838
+ })
859
839
  await this.sql`
860
840
  WITH selected AS (
861
841
  SELECT DISTINCT object_type_id, primary_id,
862
842
  ${this.sql.unsafe(objectKeyExpression("rows"))} AS identity_key,
863
843
  ${this.sql.unsafe(objectSortExpression("rows"))} AS sort_key
864
- FROM ontology_source_rows AS rows
865
- WHERE project_id = ${this.projectId}
866
- AND source_id = ${input.sourceId}
867
- AND entity_kind = 'object'
868
- AND materialization_id = ANY(${this.sql.array(materializationIds)}::text[])
844
+ FROM (${rows}) AS rows
845
+ WHERE entity_kind = 'object'
869
846
  )
870
847
  INSERT INTO ${this.sql(PG_REPLACEMENT_WORK_TABLE)} (
871
848
  session_id, entity_kind, identity_key, sort_key, diff_required
@@ -879,10 +856,13 @@ export class PgMaterializationStateReader {
879
856
  }
880
857
 
881
858
  private async prepareReplacementLinks(input: ReplacementIdentityInput): Promise<void> {
882
- const materializationIds = [
883
- input.candidateMaterializationId,
884
- ...(input.previousMaterializationId ? [input.previousMaterializationId] : []),
885
- ]
859
+ await this.sql`ANALYZE ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}`
860
+ const rows = replacementSourceRows(this.sql, {
861
+ projectId: this.projectId,
862
+ sourceId: input.sourceId,
863
+ materializationId: input.candidateMaterializationId,
864
+ incremental: input.incremental ?? false,
865
+ })
886
866
  await this.sql`
887
867
  WITH incident_objects AS (
888
868
  SELECT DISTINCT payload->'ref'->>'objectTypeId' AS object_type_id,
@@ -892,11 +872,8 @@ export class PgMaterializationStateReader {
892
872
  ), replacement_links AS (
893
873
  SELECT source_type_id, source_primary_id AS source_id, link_id,
894
874
  target_type_id, target_primary_id AS target_id
895
- FROM ontology_source_rows
896
- WHERE project_id = ${this.projectId}
897
- AND source_id = ${input.sourceId}
898
- AND entity_kind = 'link'
899
- AND materialization_id = ANY(${this.sql.array(materializationIds)}::text[])
875
+ FROM (${rows}) AS rows
876
+ WHERE entity_kind = 'link'
900
877
  ), incident_links AS (
901
878
  SELECT links.source_type_id, links.source_id, links.link_id,
902
879
  links.target_type_id, links.target_id
@@ -935,30 +912,32 @@ export class PgMaterializationStateReader {
935
912
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
936
913
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
937
914
  FROM ontology_source_rows AS rows
938
- JOIN ontology_sources AS sources
915
+ JOIN ontology_source_roots AS sources
939
916
  ON sources.project_id = rows.project_id
940
917
  AND sources.source_id = rows.source_id
941
918
  AND sources.materialization_id = rows.materialization_id
919
+ AND sources.root_sort_key = rows.root_sort_key
942
920
  JOIN incident_objects
943
921
  ON incident_objects.object_type_id = rows.source_type_id
944
922
  AND incident_objects.primary_id = rows.source_primary_id
945
923
  WHERE rows.project_id = ${this.projectId}
946
924
  AND rows.entity_kind = 'link'
947
- AND sources.status = 'active'
925
+ AND sources.active
948
926
  UNION
949
927
  SELECT rows.source_type_id, rows.source_primary_id AS source_id,
950
928
  rows.link_id, rows.target_type_id, rows.target_primary_id AS target_id
951
929
  FROM ontology_source_rows AS rows
952
- JOIN ontology_sources AS sources
930
+ JOIN ontology_source_roots AS sources
953
931
  ON sources.project_id = rows.project_id
954
932
  AND sources.source_id = rows.source_id
955
933
  AND sources.materialization_id = rows.materialization_id
934
+ AND sources.root_sort_key = rows.root_sort_key
956
935
  JOIN incident_objects
957
936
  ON incident_objects.object_type_id = rows.target_type_id
958
937
  AND incident_objects.primary_id = rows.target_primary_id
959
938
  WHERE rows.project_id = ${this.projectId}
960
939
  AND rows.entity_kind = 'link'
961
- AND sources.status = 'active'
940
+ AND sources.active
962
941
  ), diff_links AS (
963
942
  SELECT * FROM replacement_links
964
943
  UNION SELECT * FROM incident_links
@@ -68,6 +68,7 @@ import {
68
68
  type PgOntologySourceRow,
69
69
  toIsoString,
70
70
  } from "./shared"
71
+ import { activateSourceRoots } from "./source-roots"
71
72
 
72
73
  interface ProjectionCountsRow {
73
74
  readonly object_classifications: number | string
@@ -235,6 +236,7 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
235
236
  sourceId: replacement.sourceId,
236
237
  candidateMaterializationId: replacement.candidateMaterializationId,
237
238
  previousMaterializationId: replacement.previousMaterializationId,
239
+ incremental: replacement.incremental,
238
240
  kind: "object",
239
241
  pageRows: input.pageRows,
240
242
  })) {
@@ -279,6 +281,7 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
279
281
  sourceId: replacement.sourceId,
280
282
  candidateMaterializationId: replacement.candidateMaterializationId,
281
283
  previousMaterializationId: replacement.previousMaterializationId,
284
+ incremental: replacement.incremental,
282
285
  kind: "link",
283
286
  pageRows: input.pageRows,
284
287
  })) {
@@ -293,7 +296,8 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
293
296
  replacement.sourceId,
294
297
  replacement.candidateMaterializationId,
295
298
  materializationIds,
296
- linkIdentities
299
+ linkIdentities,
300
+ replacement.incremental
297
301
  )
298
302
  yield { objects: [], links }
299
303
  }
@@ -323,6 +327,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
323
327
  assertPlanChunkCorrelations(input.chunk, commit)
324
328
  const progress = await this.sessions.prepareChunkSequence(session, input.chunk)
325
329
  await this.writer.apply(commit.projectId, commit.id, input.chunk)
330
+ session.changedObjects +=
331
+ input.chunk.effective.objectUpserts.length + input.chunk.effective.objectDeletes.length
332
+ session.changedLinks +=
333
+ input.chunk.effective.linkUpserts.length + input.chunk.effective.linkDeletes.length
326
334
  this.sessions.commitChunkSequence(session, progress)
327
335
  }
328
336
 
@@ -333,6 +341,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
333
341
  for (const activation of input.finalization.sourceActivations) {
334
342
  await this.activateSource(session, activation)
335
343
  }
344
+ // The next sparse projection must see the distribution of a newly published bulk load.
345
+ // With empty/stale statistics, a link lookup can scan every edge sharing its target.
346
+ if (session.changedObjects >= 1_000) await this.sql`ANALYZE objects`
347
+ if (session.changedLinks >= 1_000) await this.sql`ANALYZE links`
336
348
  const record = await this.insertCommit(session.header, input)
337
349
  await this.sessions.release(session)
338
350
  return { commit: record }
@@ -393,6 +405,7 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
393
405
  sourceId: input.source.projectionId,
394
406
  candidateMaterializationId: input.candidateMaterializationId,
395
407
  previousMaterializationId: previous?.materialization_id ?? null,
408
+ incremental: candidate.base_materialization_id !== null,
396
409
  projectionKind: candidate.projection_kind,
397
410
  objectStreamStarted: false,
398
411
  objectStreamCompleted: false,
@@ -539,7 +552,7 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
539
552
  assertMaterializationFinalizationCorrelation(session, input)
540
553
  const laneCounts = await this.sessions.laneCounts(session)
541
554
  assertMaterializationLaneCompletion(session, laneCounts)
542
- await this.assertFinalCardinality(session)
555
+ if (laneCounts.cardinality > 0) await this.assertFinalCardinality(session)
543
556
  const eventCount = laneCounts.event
544
557
  const [outbox] = await this.sql<
545
558
  {
@@ -584,17 +597,27 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
584
597
  }
585
598
 
586
599
  private async assertFinalCardinality(session: PgMaterializationSessionState): Promise<void> {
587
- const [violation] = await this.sql<{ readonly reason: "duplicate" | "mismatch" }[]>`
588
- WITH work AS (
589
- SELECT payload->>'scopeSortKey' AS scope_sort_key,
590
- payload->>'linkSortKey' AS link_sort_key,
600
+ // A mixed work table can hide a few occupied scopes among millions of other records.
601
+ // Even ANALYZE can miss that population. Isolate it before planning the final join so
602
+ // the planner sees a relation of scopes, without JSON/session selectivity guesses.
603
+ // Session IDs are provider-generated UUIDs. Use unprepared statements for this private
604
+ // identifier so each session does not leave a new statement in the connection cache.
605
+ const table = `ontology_cardinality_${session.id.replaceAll("-", "")}`
606
+ await this.sql.unsafe(
607
+ `CREATE TEMP TABLE ${table} ON COMMIT DROP AS
608
+ SELECT sort_one AS scope_sort_key, sort_two AS link_sort_key,
591
609
  (payload->>'occupied')::boolean AS occupied,
592
610
  payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
593
611
  payload->'ref'->'source'->>'primaryId' AS source_id,
594
612
  payload->'ref'->>'linkId' AS link_id
595
- FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
596
- WHERE session_id = ${session.id} AND kind = 'cardinality'
597
- AND payload->>'view' = 'effective'
613
+ FROM ${PG_MATERIALIZATION_WORK_TABLE}
614
+ WHERE session_id = $1 AND lane = 'cardinality' AND major_order = 0`,
615
+ [session.id]
616
+ )
617
+ await this.sql.unsafe(`ANALYZE ${table}`)
618
+ const [violation] = await this.sql.unsafe<{ readonly reason: "duplicate" | "mismatch" }[]>(
619
+ `WITH work AS (
620
+ SELECT * FROM ${table}
598
621
  ), duplicate AS (
599
622
  SELECT scope_sort_key FROM work WHERE occupied
600
623
  GROUP BY scope_sort_key HAVING COUNT(*) > 1
@@ -604,10 +627,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
604
627
  SELECT scope_sort_key, link_sort_key FROM work WHERE occupied
605
628
  ), actual AS (
606
629
  SELECT scopes.scope_sort_key,
607
- ${this.sql.unsafe(linkSortExpression("links"))} AS link_sort_key
630
+ ${linkSortExpression("links")} AS link_sort_key
608
631
  FROM scopes
609
632
  JOIN links USING (source_type_id, source_id, link_id)
610
- WHERE links.project_id = ${session.header.commit.projectId}
633
+ WHERE links.project_id = $1
611
634
  ), differences AS (
612
635
  (SELECT * FROM expected EXCEPT SELECT * FROM actual)
613
636
  UNION ALL
@@ -616,8 +639,10 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
616
639
  SELECT 'duplicate'::text AS reason FROM duplicate
617
640
  UNION ALL
618
641
  SELECT 'mismatch'::text AS reason FROM differences
619
- LIMIT 1
620
- `
642
+ LIMIT 1`,
643
+ [session.header.commit.projectId]
644
+ )
645
+ await this.sql.unsafe(`DROP TABLE ${table}`)
621
646
  if (violation?.reason === "duplicate") {
622
647
  invalidCorrelation("Materialization cardinality work violates cardinality-one.")
623
648
  }
@@ -766,6 +791,7 @@ export class PgOntologyMaterializationStorage implements OntologyMaterialization
766
791
  true
767
792
  )
768
793
  this.assertSourceRow(activation.expected, previous)
794
+ await activateSourceRoots(this.sql, commit.projectId, candidate, activation)
769
795
  if (previous) {
770
796
  assertPinnedDatasetWatermark(
771
797
  {
@@ -64,6 +64,8 @@ export interface PgOntologySourceRow {
64
64
  readonly ownership_hash: string
65
65
  readonly ontology_revision: string
66
66
  readonly root_count: number | string | null
67
+ readonly base_materialization_id: string | null
68
+ readonly base_commit_id: string | null
67
69
  readonly assertion_count: number | string | null
68
70
  readonly created_at: Date | string
69
71
  readonly ready_at: Date | string | null
@@ -146,6 +148,14 @@ export function sourceRecord(row: PgOntologySourceRow): OntologySourceRecord {
146
148
  projectionRevision: row.projection_revision,
147
149
  ownershipHash: row.ownership_hash,
148
150
  ontologyRevision: row.ontology_revision,
151
+ ...(row.base_materialization_id === null
152
+ ? {}
153
+ : {
154
+ base: {
155
+ materializationId: row.base_materialization_id,
156
+ lastCommitId: row.base_commit_id!,
157
+ },
158
+ }),
149
159
  rootCount:
150
160
  row.root_count === null ? null : databaseSafeInteger(row.root_count, "Source root count"),
151
161
  assertionCount:
@@ -0,0 +1,61 @@
1
+ import type {
2
+ CleanupTerminalSourceMaterializationsInput,
3
+ CleanupTerminalSourceMaterializationsResult,
4
+ } from "@sixb/core/storage"
5
+ import type { SQLClient } from "../pg-client"
6
+
7
+ export async function cleanupSourceVersions(
8
+ sql: SQLClient,
9
+ input: CleanupTerminalSourceMaterializationsInput
10
+ ): Promise<CleanupTerminalSourceMaterializationsResult> {
11
+ let remaining = input.limit
12
+ const removedRows = await sql`
13
+ WITH selected AS (
14
+ SELECT rows.ctid FROM ontology_source_roots AS roots
15
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
16
+ JOIN ontology_source_rows AS rows
17
+ ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
18
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
19
+ WHERE roots.project_id = ${input.projectId} AND NOT roots.active AND roots.retired_at < ${input.terminalBefore}
20
+ AND sources.status IN ('superseded', 'abandoned')
21
+ ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key, rows.entity_sort_key
22
+ LIMIT ${remaining} FOR UPDATE OF rows SKIP LOCKED
23
+ ) DELETE FROM ontology_source_rows AS rows USING selected WHERE rows.ctid = selected.ctid RETURNING 1
24
+ `
25
+ let rowsDeleted = removedRows.length
26
+ remaining -= rowsDeleted
27
+ if (remaining > 0) {
28
+ const removed = await sql`
29
+ WITH selected AS (
30
+ SELECT roots.ctid FROM ontology_source_roots AS roots
31
+ JOIN ontology_sources AS sources USING (project_id, source_id, materialization_id)
32
+ WHERE roots.project_id = ${input.projectId} AND NOT roots.active AND roots.retired_at < ${input.terminalBefore}
33
+ AND sources.status IN ('superseded', 'abandoned')
34
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
35
+ WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
36
+ AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key)
37
+ ORDER BY roots.retired_at, roots.source_id, roots.materialization_id, roots.root_sort_key
38
+ LIMIT ${remaining} FOR UPDATE OF roots SKIP LOCKED
39
+ ) DELETE FROM ontology_source_roots AS roots USING selected WHERE roots.ctid = selected.ctid RETURNING 1
40
+ `
41
+ rowsDeleted += removed.length
42
+ remaining -= removed.length
43
+ }
44
+ const removedManifests =
45
+ remaining === 0
46
+ ? []
47
+ : await sql`
48
+ WITH selected AS (
49
+ SELECT sources.ctid FROM ontology_sources AS sources
50
+ WHERE project_id = ${input.projectId} AND status IN ('superseded', 'abandoned') AND terminal_at < ${input.terminalBefore}
51
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_roots AS roots
52
+ WHERE roots.project_id = sources.project_id AND roots.source_id = sources.source_id
53
+ AND roots.materialization_id = sources.materialization_id)
54
+ AND NOT EXISTS (SELECT 1 FROM ontology_source_rows AS rows
55
+ WHERE rows.project_id = sources.project_id AND rows.source_id = sources.source_id
56
+ AND rows.materialization_id = sources.materialization_id)
57
+ ORDER BY terminal_at, source_id, materialization_id LIMIT ${remaining} FOR UPDATE SKIP LOCKED
58
+ ) DELETE FROM ontology_sources AS sources USING selected WHERE sources.ctid = selected.ctid RETURNING 1
59
+ `
60
+ return { rowsDeleted, materializationsDeleted: removedManifests.length }
61
+ }