@sixb/pg 0.1.9 → 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.
- package/dist/index.js +471 -227
- package/dist/index.js.map +13 -11
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +3 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +2 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +2 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts +4 -0
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
- package/dist/ontology-storage/source-roots.d.ts +24 -0
- package/dist/ontology-storage/source-roots.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/connector-connection-storage.ts +3 -3
- package/src/migrations/038-outbox-publication-order.sql +5 -0
- package/src/migrations/039-projection-source-roots.sql +46 -0
- package/src/migrations/040-connector-optional-pkce.sql +3 -0
- package/src/migrations.ts +13 -0
- package/src/ontology-storage/materialization-session.ts +24 -8
- package/src/ontology-storage/materialization-state.ts +65 -86
- package/src/ontology-storage/materializations.ts +39 -13
- package/src/ontology-storage/shared.ts +10 -0
- package/src/ontology-storage/source-cleanup.ts +61 -0
- package/src/ontology-storage/source-roots.ts +251 -0
- package/src/ontology-storage/sources.ts +21 -57
- package/src/pg-projection-run-storage.ts +8 -2
|
@@ -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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
856
|
-
|
|
857
|
-
|
|
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
|
|
865
|
-
WHERE
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
|
896
|
-
WHERE
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 ${
|
|
596
|
-
WHERE session_id = $
|
|
597
|
-
|
|
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
|
-
${
|
|
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 = $
|
|
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
|
+
}
|