@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.
- package/dist/index.js +481 -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 +13 -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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
|
|
2
|
+
import {
|
|
3
|
+
MaterializationConflictError,
|
|
4
|
+
MaterializationValidationError,
|
|
5
|
+
projectionEntityKey,
|
|
6
|
+
} from "@sixb/core/internal/materialization"
|
|
7
|
+
import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider"
|
|
8
|
+
import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
|
|
9
|
+
import type { SQLClient } from "../pg-client"
|
|
10
|
+
import type { PgOntologySourceAssertionRow } from "./shared"
|
|
11
|
+
import { jsonParameter, type PgOntologySourceRow } from "./shared"
|
|
12
|
+
|
|
13
|
+
export function replacementSourceRows(
|
|
14
|
+
sql: SQLClient,
|
|
15
|
+
input: {
|
|
16
|
+
readonly projectId: string
|
|
17
|
+
readonly sourceId: string
|
|
18
|
+
readonly materializationId: string
|
|
19
|
+
readonly incremental: boolean
|
|
20
|
+
}
|
|
21
|
+
) {
|
|
22
|
+
const roots = input.incremental
|
|
23
|
+
? sql`
|
|
24
|
+
SELECT roots.* FROM ontology_source_roots AS changed
|
|
25
|
+
CROSS JOIN LATERAL (
|
|
26
|
+
SELECT roots.* FROM ontology_source_roots AS roots
|
|
27
|
+
WHERE roots.project_id = changed.project_id AND roots.source_id = changed.source_id
|
|
28
|
+
AND roots.root_sort_key = changed.root_sort_key AND roots.active OFFSET 0
|
|
29
|
+
) AS roots
|
|
30
|
+
WHERE changed.project_id = ${input.projectId} AND changed.source_id = ${input.sourceId}
|
|
31
|
+
AND changed.materialization_id = ${input.materializationId}
|
|
32
|
+
`
|
|
33
|
+
: sql`SELECT * FROM ontology_source_roots WHERE project_id = ${input.projectId} AND source_id = ${input.sourceId} AND active`
|
|
34
|
+
return sql`
|
|
35
|
+
SELECT * FROM ontology_source_rows WHERE project_id = ${input.projectId}
|
|
36
|
+
AND source_id = ${input.sourceId} AND materialization_id = ${input.materializationId}
|
|
37
|
+
UNION ALL
|
|
38
|
+
SELECT rows.* FROM (${roots}) AS roots
|
|
39
|
+
CROSS JOIN LATERAL (
|
|
40
|
+
SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.project_id = roots.project_id
|
|
41
|
+
AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
|
|
42
|
+
AND rows.root_sort_key = roots.root_sort_key OFFSET 0
|
|
43
|
+
) AS rows
|
|
44
|
+
`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function stageSourceRoots(
|
|
48
|
+
sql: SQLClient,
|
|
49
|
+
manifest: PgOntologySourceRow,
|
|
50
|
+
input: StageSourceRowsInput
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const roots = sourceStageRoots(manifest.projection_kind, input)
|
|
53
|
+
if (roots.length === 0) return
|
|
54
|
+
if (!manifest.base_materialization_id && roots.some((root) => root.deleted)) {
|
|
55
|
+
throw new MaterializationValidationError("Root deletions require a source delta base.")
|
|
56
|
+
}
|
|
57
|
+
const values = jsonParameter(
|
|
58
|
+
sql,
|
|
59
|
+
roots.map((root) => ({
|
|
60
|
+
root_sort_key: root.sortKey,
|
|
61
|
+
root_kind: root.root.kind,
|
|
62
|
+
root_key: JSON.parse(root.rootKey),
|
|
63
|
+
root: root.root,
|
|
64
|
+
staging_ordinal: root.stagingOrdinal,
|
|
65
|
+
deleted: root.deleted,
|
|
66
|
+
}))
|
|
67
|
+
)
|
|
68
|
+
const inserted = await sql<{ root_sort_key: string }[]>`
|
|
69
|
+
INSERT INTO ontology_source_roots (
|
|
70
|
+
project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
|
|
71
|
+
) SELECT ${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
|
|
72
|
+
root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
|
|
73
|
+
FROM jsonb_to_recordset(${values}) AS input(
|
|
74
|
+
root_sort_key TEXT, root_kind TEXT, root_key JSONB, root JSONB, staging_ordinal BIGINT, deleted BOOLEAN)
|
|
75
|
+
ON CONFLICT DO NOTHING RETURNING root_sort_key
|
|
76
|
+
`
|
|
77
|
+
if (inserted.length === roots.length) return
|
|
78
|
+
const insertedKeys = new Set(inserted.map((row) => row.root_sort_key))
|
|
79
|
+
const skipped = roots.filter((root) => !insertedKeys.has(root.sortKey))
|
|
80
|
+
const stored = await sql<
|
|
81
|
+
{ root_sort_key: string; staging_ordinal: string | number; deleted: boolean }[]
|
|
82
|
+
>`
|
|
83
|
+
SELECT root_sort_key, staging_ordinal, deleted FROM ontology_source_roots
|
|
84
|
+
WHERE project_id = ${input.projectId} AND source_id = ${input.source.projectionId}
|
|
85
|
+
AND materialization_id = ${input.materializationId}
|
|
86
|
+
AND root_sort_key = ANY(${sql.array(skipped.map((root) => root.sortKey))}::text[])
|
|
87
|
+
`
|
|
88
|
+
const existing = new Map(stored.map((root) => [root.root_sort_key, root]))
|
|
89
|
+
if (
|
|
90
|
+
skipped.some((root) => {
|
|
91
|
+
const previous = existing.get(root.sortKey)
|
|
92
|
+
return (
|
|
93
|
+
!previous ||
|
|
94
|
+
Number(previous.staging_ordinal) !== root.stagingOrdinal ||
|
|
95
|
+
previous.deleted !== root.deleted
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
throw new MaterializationValidationError(
|
|
100
|
+
"Source materialization repeats root or stream ordinal with different content."
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function activateSourceRoots(
|
|
105
|
+
sql: SQLClient,
|
|
106
|
+
projectId: string,
|
|
107
|
+
candidate: PgOntologySourceRow,
|
|
108
|
+
activation: SourceActivationWrite
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
if (
|
|
111
|
+
candidate.base_materialization_id !== null &&
|
|
112
|
+
(candidate.base_materialization_id !== activation.expected.activeMaterializationId ||
|
|
113
|
+
candidate.base_commit_id !== activation.expected.lastCommitId)
|
|
114
|
+
)
|
|
115
|
+
throw new MaterializationConflictError(
|
|
116
|
+
"projection-fence",
|
|
117
|
+
"Source delta base changed before activation."
|
|
118
|
+
)
|
|
119
|
+
if (candidate.base_materialization_id !== null && Number(candidate.root_count) === 0) return
|
|
120
|
+
const affected =
|
|
121
|
+
candidate.base_materialization_id === null
|
|
122
|
+
? sql``
|
|
123
|
+
: sql`AND root_sort_key IN (
|
|
124
|
+
SELECT root_sort_key FROM ontology_source_roots
|
|
125
|
+
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
|
|
126
|
+
AND materialization_id = ${activation.materializationId}
|
|
127
|
+
)`
|
|
128
|
+
await sql`
|
|
129
|
+
UPDATE ontology_source_roots AS roots SET active = FALSE, retired_at = ${activation.updatedAt}
|
|
130
|
+
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId} AND active
|
|
131
|
+
${affected}
|
|
132
|
+
`
|
|
133
|
+
await sql`
|
|
134
|
+
UPDATE ontology_source_roots SET active = NOT deleted,
|
|
135
|
+
retired_at = CASE WHEN deleted THEN ${activation.updatedAt}::timestamptz ELSE NULL END
|
|
136
|
+
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
|
|
137
|
+
AND materialization_id = ${activation.materializationId}
|
|
138
|
+
`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function assertSourceRootCoverage(
|
|
142
|
+
sql: SQLClient,
|
|
143
|
+
manifest: PgOntologySourceRow
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const [invalid] = await sql`
|
|
146
|
+
SELECT 1 FROM ontology_source_roots AS roots
|
|
147
|
+
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
148
|
+
AND materialization_id = ${manifest.materialization_id}
|
|
149
|
+
AND roots.deleted = EXISTS (
|
|
150
|
+
SELECT 1 FROM ontology_source_rows AS rows
|
|
151
|
+
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
152
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
153
|
+
) LIMIT 1
|
|
154
|
+
`
|
|
155
|
+
if (invalid)
|
|
156
|
+
throw new MaterializationValidationError(
|
|
157
|
+
"Source roots must have complete assertions or an explicit deletion, never both."
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function activeSourceRows(
|
|
162
|
+
sql: SQLClient,
|
|
163
|
+
projectId: string,
|
|
164
|
+
refs: readonly ProjectionEntityRef[]
|
|
165
|
+
) {
|
|
166
|
+
const keys = refs.flatMap((entity) => {
|
|
167
|
+
const entitySortKey = utf8SortKey(projectionEntityKey(entity))
|
|
168
|
+
const roots =
|
|
169
|
+
entity.kind === "object"
|
|
170
|
+
? [entitySortKey]
|
|
171
|
+
: [
|
|
172
|
+
entitySortKey,
|
|
173
|
+
utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source })),
|
|
174
|
+
]
|
|
175
|
+
return roots.map((rootSortKey) => ({
|
|
176
|
+
root_sort_key: rootSortKey,
|
|
177
|
+
entity_sort_key: entitySortKey,
|
|
178
|
+
}))
|
|
179
|
+
})
|
|
180
|
+
return sql<PgOntologySourceAssertionRow[]>`
|
|
181
|
+
SELECT rows.* FROM jsonb_to_recordset(${jsonParameter(sql, keys)}) AS requested(root_sort_key TEXT, entity_sort_key TEXT)
|
|
182
|
+
JOIN ontology_source_roots AS roots ON roots.project_id = ${projectId}
|
|
183
|
+
AND roots.active AND roots.root_sort_key = requested.root_sort_key
|
|
184
|
+
JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
185
|
+
AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
|
|
186
|
+
AND rows.root_sort_key = roots.root_sort_key
|
|
187
|
+
`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function replacementAssertionRows(
|
|
191
|
+
sql: SQLClient,
|
|
192
|
+
input: {
|
|
193
|
+
projectId: string
|
|
194
|
+
sourceId: string
|
|
195
|
+
materializationId: string
|
|
196
|
+
incremental: boolean
|
|
197
|
+
includePrevious: boolean
|
|
198
|
+
refs: readonly ProjectionEntityRef[]
|
|
199
|
+
}
|
|
200
|
+
) {
|
|
201
|
+
const keys = input.refs.map((ref) => ({
|
|
202
|
+
entity_kind: ref.kind,
|
|
203
|
+
entity_key: JSON.parse(projectionEntityKey(ref)),
|
|
204
|
+
root_keys:
|
|
205
|
+
ref.kind === "object"
|
|
206
|
+
? [utf8SortKey(projectionEntityKey(ref))]
|
|
207
|
+
: [
|
|
208
|
+
utf8SortKey(projectionEntityKey(ref)),
|
|
209
|
+
utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source })),
|
|
210
|
+
],
|
|
211
|
+
}))
|
|
212
|
+
const previous = !input.includePrevious
|
|
213
|
+
? sql``
|
|
214
|
+
: sql`UNION ALL
|
|
215
|
+
SELECT rows.* FROM (
|
|
216
|
+
SELECT roots.* FROM ontology_source_roots AS roots
|
|
217
|
+
WHERE roots.project_id = ${input.projectId} AND roots.source_id = ${input.sourceId}
|
|
218
|
+
AND roots.active AND roots.root_sort_key = ANY(requested.root_keys)
|
|
219
|
+
${
|
|
220
|
+
!input.incremental
|
|
221
|
+
? sql``
|
|
222
|
+
: sql`AND EXISTS (SELECT 1 FROM ontology_source_roots AS changed
|
|
223
|
+
WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
|
|
224
|
+
AND changed.materialization_id = ${input.materializationId} AND changed.root_sort_key = roots.root_sort_key)`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
OFFSET 0
|
|
228
|
+
) AS roots
|
|
229
|
+
CROSS JOIN LATERAL (
|
|
230
|
+
SELECT rows.* FROM ontology_source_rows AS rows
|
|
231
|
+
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
232
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
233
|
+
AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
234
|
+
OFFSET 0
|
|
235
|
+
) AS rows
|
|
236
|
+
`
|
|
237
|
+
// Keep each JSON request as an indexed identity lookup even with fresh/stale statistics.
|
|
238
|
+
// Flattening this LATERAL caused a cold 10k-root run to spend seconds on each state page.
|
|
239
|
+
return sql<PgOntologySourceAssertionRow[]>`
|
|
240
|
+
SELECT selected.* FROM jsonb_to_recordset(${jsonParameter(sql, keys)})
|
|
241
|
+
AS requested(entity_kind TEXT, entity_key JSONB, root_keys TEXT[])
|
|
242
|
+
CROSS JOIN LATERAL (
|
|
243
|
+
SELECT rows.* FROM ontology_source_rows AS rows
|
|
244
|
+
WHERE rows.project_id = ${input.projectId} AND rows.source_id = ${input.sourceId}
|
|
245
|
+
AND rows.materialization_id = ${input.materializationId}
|
|
246
|
+
AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
247
|
+
${previous}
|
|
248
|
+
OFFSET 0
|
|
249
|
+
) AS selected
|
|
250
|
+
`
|
|
251
|
+
}
|
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
sourceRecord,
|
|
50
50
|
toIsoString,
|
|
51
51
|
} from "./shared"
|
|
52
|
+
import { cleanupSourceVersions } from "./source-cleanup"
|
|
53
|
+
import { assertSourceRootCoverage, stageSourceRoots } from "./source-roots"
|
|
52
54
|
|
|
53
55
|
export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
54
56
|
constructor(private readonly runRootOperation: PgRootOperation) {}
|
|
@@ -97,14 +99,15 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
97
99
|
dataset_id, dataset_version_id, dataset_version_created_at,
|
|
98
100
|
projection_revision, ownership_hash, ontology_revision,
|
|
99
101
|
root_count, assertion_count, created_at, ready_at, activated_at,
|
|
100
|
-
terminal_at, last_commit_id, updated_at
|
|
102
|
+
terminal_at, last_commit_id, updated_at, base_materialization_id, base_commit_id
|
|
101
103
|
) VALUES (
|
|
102
104
|
${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
|
|
103
105
|
${input.execution.projectionRunId}, ${input.projectionKind}, 'replacement',
|
|
104
106
|
'staging', ${input.execution.executionToken}, ${input.datasetVersion.datasetId},
|
|
105
107
|
${input.datasetVersion.versionId}, ${input.datasetVersion.createdAt},
|
|
106
108
|
${input.projectionRevision}, ${input.ownershipHash}, ${input.ontologyRevision},
|
|
107
|
-
NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt}
|
|
109
|
+
NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt},
|
|
110
|
+
${input.base?.materializationId ?? null}, ${input.base?.lastCommitId ?? null}
|
|
108
111
|
)
|
|
109
112
|
ON CONFLICT DO NOTHING
|
|
110
113
|
RETURNING *
|
|
@@ -153,6 +156,7 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
153
156
|
const rows = sourceStageRows(manifest.projection_kind, input.rows)
|
|
154
157
|
const existing = await this.findStageRows(sql, input, rows)
|
|
155
158
|
const { pending, unchanged } = reconcileSourceStageRows(rows, existing)
|
|
159
|
+
await stageSourceRoots(sql, manifest, input)
|
|
156
160
|
await this.insertStageRows(sql, input, pending)
|
|
157
161
|
return { inserted: pending.length, unchanged }
|
|
158
162
|
})
|
|
@@ -193,6 +197,12 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
193
197
|
if (input.readyAt < toIsoString(manifest.created_at)) {
|
|
194
198
|
throw new MaterializationValidationError("Source readyAt cannot precede source createdAt.")
|
|
195
199
|
}
|
|
200
|
+
// A newly loaded run is absent from the planner statistics. Without refreshing them,
|
|
201
|
+
// PostgreSQL can scan the whole run for each identity instead of using its full key.
|
|
202
|
+
// Sparse deltas avoid this table-level sampling cost.
|
|
203
|
+
if (input.assertionCount >= 1_000 || input.rootCount >= 1_000) {
|
|
204
|
+
await sql`ANALYZE ontology_source_rows, ontology_source_roots`
|
|
205
|
+
}
|
|
196
206
|
await this.assertReadyState(sql, manifest, input.rootCount, input.assertionCount)
|
|
197
207
|
const rows = await sql<PgOntologySourceRow[]>`
|
|
198
208
|
UPDATE ontology_sources
|
|
@@ -247,59 +257,7 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
247
257
|
assertNonblank(input.projectId, "Terminal source cleanup project id")
|
|
248
258
|
assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true)
|
|
249
259
|
assertPositiveInteger(input.limit, "Terminal source cleanup limit")
|
|
250
|
-
|
|
251
|
-
SELECT * FROM ontology_sources
|
|
252
|
-
WHERE project_id = ${input.projectId}
|
|
253
|
-
AND status IN ('superseded', 'abandoned')
|
|
254
|
-
AND terminal_at < ${input.terminalBefore}
|
|
255
|
-
ORDER BY terminal_at, source_id, materialization_id
|
|
256
|
-
LIMIT ${input.limit}
|
|
257
|
-
FOR UPDATE SKIP LOCKED
|
|
258
|
-
`
|
|
259
|
-
let remaining = input.limit
|
|
260
|
-
let rowsDeleted = 0
|
|
261
|
-
let materializationsDeleted = 0
|
|
262
|
-
for (const manifest of manifests) {
|
|
263
|
-
if (remaining === 0) break
|
|
264
|
-
const deletedRows = await sql<{ readonly entity_sort_key: string }[]>`
|
|
265
|
-
WITH selected AS (
|
|
266
|
-
SELECT rows.ctid
|
|
267
|
-
FROM ontology_source_rows AS rows
|
|
268
|
-
WHERE rows.project_id = ${manifest.project_id}
|
|
269
|
-
AND rows.source_id = ${manifest.source_id}
|
|
270
|
-
AND rows.materialization_id = ${manifest.materialization_id}
|
|
271
|
-
ORDER BY rows.entity_sort_key
|
|
272
|
-
LIMIT ${remaining}
|
|
273
|
-
FOR UPDATE SKIP LOCKED
|
|
274
|
-
)
|
|
275
|
-
DELETE FROM ontology_source_rows AS rows
|
|
276
|
-
USING selected
|
|
277
|
-
WHERE rows.ctid = selected.ctid
|
|
278
|
-
RETURNING rows.entity_sort_key
|
|
279
|
-
`
|
|
280
|
-
rowsDeleted += deletedRows.length
|
|
281
|
-
remaining -= deletedRows.length
|
|
282
|
-
if (remaining === 0) break
|
|
283
|
-
const removed = await sql<{ readonly materialization_id: string }[]>`
|
|
284
|
-
DELETE FROM ontology_sources AS sources
|
|
285
|
-
WHERE sources.project_id = ${manifest.project_id}
|
|
286
|
-
AND sources.source_id = ${manifest.source_id}
|
|
287
|
-
AND sources.materialization_id = ${manifest.materialization_id}
|
|
288
|
-
AND sources.status IN ('superseded', 'abandoned')
|
|
289
|
-
AND sources.terminal_at = ${manifest.terminal_at}
|
|
290
|
-
AND sources.terminal_at < ${input.terminalBefore}
|
|
291
|
-
AND NOT EXISTS (
|
|
292
|
-
SELECT 1 FROM ontology_source_rows AS rows
|
|
293
|
-
WHERE rows.project_id = sources.project_id
|
|
294
|
-
AND rows.source_id = sources.source_id
|
|
295
|
-
AND rows.materialization_id = sources.materialization_id
|
|
296
|
-
)
|
|
297
|
-
RETURNING sources.materialization_id
|
|
298
|
-
`
|
|
299
|
-
materializationsDeleted += removed.length
|
|
300
|
-
remaining -= removed.length
|
|
301
|
-
}
|
|
302
|
-
return { rowsDeleted, materializationsDeleted }
|
|
260
|
+
return cleanupSourceVersions(sql, input)
|
|
303
261
|
})
|
|
304
262
|
}
|
|
305
263
|
|
|
@@ -408,6 +366,9 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
408
366
|
`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`
|
|
409
367
|
)
|
|
410
368
|
}
|
|
369
|
+
await sql`UPDATE ontology_source_roots SET retired_at = ${abandonedAt}
|
|
370
|
+
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
371
|
+
AND materialization_id = ${manifest.materialization_id} AND NOT active`
|
|
411
372
|
return sourceRecord(rows[0])
|
|
412
373
|
}
|
|
413
374
|
|
|
@@ -426,10 +387,12 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
426
387
|
readonly max_ordinal: number | string | null
|
|
427
388
|
}[]
|
|
428
389
|
>`
|
|
429
|
-
SELECT
|
|
390
|
+
SELECT (SELECT COUNT(*) FROM ontology_source_rows
|
|
391
|
+
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
392
|
+
AND materialization_id = ${manifest.materialization_id}) AS assertions, COUNT(*) AS roots,
|
|
430
393
|
COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
431
394
|
MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
|
|
432
|
-
FROM
|
|
395
|
+
FROM ontology_source_roots
|
|
433
396
|
WHERE project_id = ${manifest.project_id}
|
|
434
397
|
AND source_id = ${manifest.source_id}
|
|
435
398
|
AND materialization_id = ${manifest.materialization_id}
|
|
@@ -449,6 +412,7 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
449
412
|
"Source ready counts do not match the staged roots and assertions."
|
|
450
413
|
)
|
|
451
414
|
}
|
|
415
|
+
await assertSourceRootCoverage(sql, manifest)
|
|
452
416
|
|
|
453
417
|
const [invalid] =
|
|
454
418
|
manifest.projection_kind === "link"
|
|
@@ -102,7 +102,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
102
102
|
input_exhausted = ${input.fixedBatchSize === undefined ? null : false},
|
|
103
103
|
missing_target_object_type_id = ${null}, missing_target_object_id = ${null},
|
|
104
104
|
missing_target_batch_ordinal = ${null}, missing_target_first_seen_at = ${null},
|
|
105
|
-
source_rows_read = ${0}, source_rows_skipped = ${0}, error = ${null}
|
|
105
|
+
source_rows_read = ${0}, source_rows_skipped = ${0}, source_changes_read = ${null}, error = ${null}
|
|
106
106
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
107
107
|
AND status = ${"failed"} AND error->>'code' = ${"queue.enqueue_failed"}
|
|
108
108
|
RETURNING *
|
|
@@ -216,7 +216,8 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
216
216
|
UPDATE projection_runs
|
|
217
217
|
SET
|
|
218
218
|
source_rows_read = ${progress.sourceRowsRead},
|
|
219
|
-
source_rows_skipped = ${progress.sourceRowsSkipped}
|
|
219
|
+
source_rows_skipped = ${progress.sourceRowsSkipped},
|
|
220
|
+
source_changes_read = ${progress.sourceChangesRead ?? null}
|
|
220
221
|
WHERE project_id = ${input.projectId}
|
|
221
222
|
AND id = ${input.id}
|
|
222
223
|
AND status = ${"running"}
|
|
@@ -241,6 +242,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
241
242
|
execution_token = ${null},
|
|
242
243
|
source_rows_read = ${plan.progress.sourceRowsRead},
|
|
243
244
|
source_rows_skipped = ${plan.progress.sourceRowsSkipped},
|
|
245
|
+
source_changes_read = ${plan.progress.sourceChangesRead ?? null},
|
|
244
246
|
input_exhausted = ${plan.inputExhausted ?? existingRow.input_exhausted},
|
|
245
247
|
error = ${plan.error === undefined ? null : serializeSixbFailure(plan.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
246
248
|
WHERE project_id = ${input.projectId}
|
|
@@ -529,6 +531,9 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
529
531
|
progress: {
|
|
530
532
|
sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
|
|
531
533
|
sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
|
|
534
|
+
...(row.source_changes_read == null
|
|
535
|
+
? {}
|
|
536
|
+
: { sourceChangesRead: databaseSafeInteger(row.source_changes_read, "sourceChangesRead") }),
|
|
532
537
|
},
|
|
533
538
|
error: row.error ?? undefined,
|
|
534
539
|
}
|
|
@@ -584,6 +589,7 @@ interface DatabaseRow {
|
|
|
584
589
|
missing_target_first_seen_at: Date | string | null
|
|
585
590
|
source_rows_read: number | string
|
|
586
591
|
source_rows_skipped: number | string
|
|
592
|
+
source_changes_read: number | string | null
|
|
587
593
|
error: JsonValue | null
|
|
588
594
|
}
|
|
589
595
|
|