@sixb/pg 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- package/src/transactions.ts +58 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { MaterializationValidationError } from "@sixb/core/internal/materialization"
|
|
2
|
+
import {
|
|
3
|
+
assertSourceBeginInput as assertBeginInput,
|
|
4
|
+
assertSourceExecutionIdentity as assertExecutionIdentity,
|
|
5
|
+
assertSourceProject as assertProjectAndSource,
|
|
6
|
+
assertSourceCandidateOwner,
|
|
7
|
+
assertSourceWriteIdentity as assertWriteIdentity,
|
|
8
|
+
sourceEntityColumns as entityColumns,
|
|
9
|
+
isExactStagingManifest,
|
|
10
|
+
reconcileSourceStageRows,
|
|
11
|
+
type SourceStageRow,
|
|
12
|
+
sourceConflict,
|
|
13
|
+
sourceMaterializationIdentity,
|
|
14
|
+
sourceStageRow,
|
|
15
|
+
sourceStageRows,
|
|
16
|
+
utf8SortKey,
|
|
17
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
18
|
+
import type {
|
|
19
|
+
AbandonSourceMaterializationCandidateInput,
|
|
20
|
+
AbandonSourceMaterializationInput,
|
|
21
|
+
AssertSourceMaterializationExecutionInput,
|
|
22
|
+
BeginSourceMaterializationInput,
|
|
23
|
+
CleanupTerminalSourceMaterializationsInput,
|
|
24
|
+
CleanupTerminalSourceMaterializationsResult,
|
|
25
|
+
GetActiveOntologySourceInput,
|
|
26
|
+
MarkSourceMaterializationReadyInput,
|
|
27
|
+
OntologySourceRecord,
|
|
28
|
+
OntologySourceStorage,
|
|
29
|
+
ReclaimSourceMaterializationInput,
|
|
30
|
+
StageSourceRowsInput,
|
|
31
|
+
StageSourceRowsResult,
|
|
32
|
+
SummarizeTerminalSourceMaterializationsInput,
|
|
33
|
+
TerminalSourceMaterializationSummary,
|
|
34
|
+
} from "@sixb/core/storage"
|
|
35
|
+
import type { SQLClient } from "../pg-client"
|
|
36
|
+
import { lockAdvisoryKeys } from "../transactions"
|
|
37
|
+
import {
|
|
38
|
+
assertNonblank,
|
|
39
|
+
assertNonnegativeInteger,
|
|
40
|
+
assertPositiveInteger,
|
|
41
|
+
assertProjectionExecution,
|
|
42
|
+
assertTimestamp,
|
|
43
|
+
jsonParameter,
|
|
44
|
+
ontologyLockKey,
|
|
45
|
+
type PgOntologySourceAssertionRow,
|
|
46
|
+
type PgOntologySourceRow,
|
|
47
|
+
type PgRootOperation,
|
|
48
|
+
sourceAssertion,
|
|
49
|
+
sourceRecord,
|
|
50
|
+
toIsoString,
|
|
51
|
+
} from "./shared"
|
|
52
|
+
|
|
53
|
+
export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
54
|
+
constructor(private readonly runRootOperation: PgRootOperation) {}
|
|
55
|
+
|
|
56
|
+
async beginMaterialization(
|
|
57
|
+
input: BeginSourceMaterializationInput
|
|
58
|
+
): Promise<OntologySourceRecord> {
|
|
59
|
+
return this.runRootOperation(async (sql) => {
|
|
60
|
+
assertBeginInput(input)
|
|
61
|
+
await this.assertExecution(sql, input, sourceMaterializationIdentity(input))
|
|
62
|
+
await lockAdvisoryKeys(sql, [
|
|
63
|
+
ontologyLockKey("source-candidate", input.projectId, input.execution.projectionRunId),
|
|
64
|
+
ontologyLockKey(
|
|
65
|
+
"source-materialization",
|
|
66
|
+
input.projectId,
|
|
67
|
+
input.source.projectionId,
|
|
68
|
+
input.materializationId
|
|
69
|
+
),
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
const existing = await this.getManifest(
|
|
73
|
+
sql,
|
|
74
|
+
input.projectId,
|
|
75
|
+
input.source.projectionId,
|
|
76
|
+
input.materializationId,
|
|
77
|
+
true
|
|
78
|
+
)
|
|
79
|
+
if (existing) {
|
|
80
|
+
if (isExactStagingManifest(sourceRecord(existing), input)) return sourceRecord(existing)
|
|
81
|
+
throw sourceConflict(
|
|
82
|
+
`Source materialization '${input.materializationId}' already exists with different identity or state.`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const candidate = await this.getRunCandidate(sql, input)
|
|
87
|
+
if (candidate) {
|
|
88
|
+
throw sourceConflict(
|
|
89
|
+
`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rows = await sql<PgOntologySourceRow[]>`
|
|
94
|
+
INSERT INTO ontology_sources (
|
|
95
|
+
project_id, source_id, materialization_id, projection_run_id,
|
|
96
|
+
projection_kind, protocol, status, execution_token,
|
|
97
|
+
dataset_id, dataset_version_id, dataset_version_created_at,
|
|
98
|
+
projection_revision, ownership_hash, ontology_revision,
|
|
99
|
+
root_count, assertion_count, created_at, ready_at, activated_at,
|
|
100
|
+
terminal_at, last_commit_id, updated_at
|
|
101
|
+
) VALUES (
|
|
102
|
+
${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
|
|
103
|
+
${input.execution.projectionRunId}, ${input.projectionKind}, 'replacement',
|
|
104
|
+
'staging', ${input.execution.executionToken}, ${input.datasetVersion.datasetId},
|
|
105
|
+
${input.datasetVersion.versionId}, ${input.datasetVersion.createdAt},
|
|
106
|
+
${input.projectionRevision}, ${input.ownershipHash}, ${input.ontologyRevision},
|
|
107
|
+
NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt}
|
|
108
|
+
)
|
|
109
|
+
ON CONFLICT DO NOTHING
|
|
110
|
+
RETURNING *
|
|
111
|
+
`
|
|
112
|
+
if (rows[0]) return sourceRecord(rows[0])
|
|
113
|
+
|
|
114
|
+
const racedManifest = await this.getManifest(
|
|
115
|
+
sql,
|
|
116
|
+
input.projectId,
|
|
117
|
+
input.source.projectionId,
|
|
118
|
+
input.materializationId,
|
|
119
|
+
true
|
|
120
|
+
)
|
|
121
|
+
if (racedManifest && isExactStagingManifest(sourceRecord(racedManifest), input)) {
|
|
122
|
+
return sourceRecord(racedManifest)
|
|
123
|
+
}
|
|
124
|
+
if (await this.getRunCandidate(sql, input)) {
|
|
125
|
+
throw sourceConflict(
|
|
126
|
+
`Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
throw sourceConflict(
|
|
130
|
+
`Source materialization '${input.materializationId}' already exists with different identity or state.`
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async stageRows(input: StageSourceRowsInput): Promise<StageSourceRowsResult> {
|
|
136
|
+
return this.runRootOperation(async (sql) => {
|
|
137
|
+
assertWriteIdentity(input)
|
|
138
|
+
await this.assertExecution(sql, input)
|
|
139
|
+
const manifest = await this.requireManifest(
|
|
140
|
+
sql,
|
|
141
|
+
input.projectId,
|
|
142
|
+
input.source.projectionId,
|
|
143
|
+
input.materializationId,
|
|
144
|
+
true
|
|
145
|
+
)
|
|
146
|
+
assertSourceCandidateOwner(sourceRecord(manifest), input.execution)
|
|
147
|
+
if (manifest.status !== "staging") {
|
|
148
|
+
throw sourceConflict(
|
|
149
|
+
`Source materialization '${input.materializationId}' is '${manifest.status}' and cannot accept rows.`
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rows = sourceStageRows(manifest.projection_kind, input.rows)
|
|
154
|
+
const existing = await this.findStageRows(sql, input, rows)
|
|
155
|
+
const { pending, unchanged } = reconcileSourceStageRows(rows, existing)
|
|
156
|
+
await this.insertStageRows(sql, input, pending)
|
|
157
|
+
return { inserted: pending.length, unchanged }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async markReady(input: MarkSourceMaterializationReadyInput): Promise<OntologySourceRecord> {
|
|
162
|
+
return this.runRootOperation(async (sql) => {
|
|
163
|
+
assertWriteIdentity(input)
|
|
164
|
+
assertTimestamp(input.readyAt, "Source readyAt", true)
|
|
165
|
+
assertNonnegativeInteger(input.rootCount, "Source root count")
|
|
166
|
+
assertNonnegativeInteger(input.assertionCount, "Source assertion count")
|
|
167
|
+
await this.assertExecution(sql, input)
|
|
168
|
+
const manifest = await this.requireManifest(
|
|
169
|
+
sql,
|
|
170
|
+
input.projectId,
|
|
171
|
+
input.source.projectionId,
|
|
172
|
+
input.materializationId,
|
|
173
|
+
true
|
|
174
|
+
)
|
|
175
|
+
assertSourceCandidateOwner(sourceRecord(manifest), input.execution)
|
|
176
|
+
if (manifest.status === "ready") {
|
|
177
|
+
if (
|
|
178
|
+
numberOrNull(manifest.root_count) === input.rootCount &&
|
|
179
|
+
numberOrNull(manifest.assertion_count) === input.assertionCount &&
|
|
180
|
+
toIsoString(manifest.ready_at!) === input.readyAt
|
|
181
|
+
) {
|
|
182
|
+
return sourceRecord(manifest)
|
|
183
|
+
}
|
|
184
|
+
throw sourceConflict(
|
|
185
|
+
`Source materialization '${input.materializationId}' was marked ready with different counts or time.`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
if (manifest.status !== "staging") {
|
|
189
|
+
throw sourceConflict(
|
|
190
|
+
`Source materialization '${input.materializationId}' cannot transition from '${manifest.status}' to 'ready'.`
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
if (input.readyAt < toIsoString(manifest.created_at)) {
|
|
194
|
+
throw new MaterializationValidationError("Source readyAt cannot precede source createdAt.")
|
|
195
|
+
}
|
|
196
|
+
await this.assertReadyState(sql, manifest, input.rootCount, input.assertionCount)
|
|
197
|
+
const rows = await sql<PgOntologySourceRow[]>`
|
|
198
|
+
UPDATE ontology_sources
|
|
199
|
+
SET status = 'ready', root_count = ${input.rootCount},
|
|
200
|
+
assertion_count = ${input.assertionCount}, ready_at = ${input.readyAt},
|
|
201
|
+
updated_at = ${input.readyAt}
|
|
202
|
+
WHERE project_id = ${input.projectId}
|
|
203
|
+
AND source_id = ${input.source.projectionId}
|
|
204
|
+
AND materialization_id = ${input.materializationId}
|
|
205
|
+
AND status = 'staging'
|
|
206
|
+
AND execution_token = ${input.execution.executionToken}
|
|
207
|
+
RETURNING *
|
|
208
|
+
`
|
|
209
|
+
if (!rows[0]) {
|
|
210
|
+
throw sourceConflict(`Source materialization '${input.materializationId}' changed.`)
|
|
211
|
+
}
|
|
212
|
+
return sourceRecord(rows[0])
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async getActive(input: GetActiveOntologySourceInput): Promise<OntologySourceRecord | null> {
|
|
217
|
+
return this.runRootOperation(async (sql) => {
|
|
218
|
+
assertProjectAndSource(input)
|
|
219
|
+
const [row] = await sql<PgOntologySourceRow[]>`
|
|
220
|
+
SELECT * FROM ontology_sources
|
|
221
|
+
WHERE project_id = ${input.projectId}
|
|
222
|
+
AND source_id = ${input.source.projectionId}
|
|
223
|
+
AND status = 'active'
|
|
224
|
+
`
|
|
225
|
+
return row ? sourceRecord(row) : null
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async abandon(input: AbandonSourceMaterializationCandidateInput): Promise<OntologySourceRecord>
|
|
230
|
+
async abandon(input: ReclaimSourceMaterializationInput): Promise<OntologySourceRecord | null>
|
|
231
|
+
async abandon(input: AbandonSourceMaterializationInput): Promise<OntologySourceRecord | null> {
|
|
232
|
+
return this.runRootOperation(async (sql) => {
|
|
233
|
+
assertProjectAndSource(input)
|
|
234
|
+
assertExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken)
|
|
235
|
+
assertTimestamp(input.abandonedAt, "Source abandonedAt", true)
|
|
236
|
+
await this.assertExecution(sql, input)
|
|
237
|
+
return input.kind === "candidate"
|
|
238
|
+
? this.abandonCandidate(sql, input)
|
|
239
|
+
: this.abandonForReclaim(sql, input)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async cleanupTerminal(
|
|
244
|
+
input: CleanupTerminalSourceMaterializationsInput
|
|
245
|
+
): Promise<CleanupTerminalSourceMaterializationsResult> {
|
|
246
|
+
return this.runRootOperation(async (sql) => {
|
|
247
|
+
assertNonblank(input.projectId, "Terminal source cleanup project id")
|
|
248
|
+
assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true)
|
|
249
|
+
assertPositiveInteger(input.limit, "Terminal source cleanup limit")
|
|
250
|
+
const manifests = await sql<PgOntologySourceRow[]>`
|
|
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 }
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async summarizeTerminal(
|
|
307
|
+
input: SummarizeTerminalSourceMaterializationsInput
|
|
308
|
+
): Promise<TerminalSourceMaterializationSummary> {
|
|
309
|
+
return this.runRootOperation(async (sql) => {
|
|
310
|
+
assertNonblank(input.projectId, "Terminal source summary project id")
|
|
311
|
+
const [summary] = await sql<
|
|
312
|
+
{
|
|
313
|
+
readonly terminal_count: number
|
|
314
|
+
readonly oldest_terminal_at: Date | string | null
|
|
315
|
+
}[]
|
|
316
|
+
>`
|
|
317
|
+
SELECT COUNT(*)::int AS terminal_count, MIN(terminal_at) AS oldest_terminal_at
|
|
318
|
+
FROM ontology_sources
|
|
319
|
+
WHERE project_id = ${input.projectId}
|
|
320
|
+
AND status IN ('superseded', 'abandoned')
|
|
321
|
+
`
|
|
322
|
+
return {
|
|
323
|
+
count: summary?.terminal_count ?? 0,
|
|
324
|
+
oldestTerminalAt: toOptionalIsoString(summary?.oldest_terminal_at),
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async abandonCandidate(
|
|
330
|
+
sql: SQLClient,
|
|
331
|
+
input: AbandonSourceMaterializationCandidateInput
|
|
332
|
+
): Promise<OntologySourceRecord> {
|
|
333
|
+
const manifest = await this.requireManifest(
|
|
334
|
+
sql,
|
|
335
|
+
input.projectId,
|
|
336
|
+
input.source.projectionId,
|
|
337
|
+
input.materializationId,
|
|
338
|
+
true
|
|
339
|
+
)
|
|
340
|
+
if (manifest.status === "abandoned") {
|
|
341
|
+
if (
|
|
342
|
+
manifest.projection_run_id === input.execution.projectionRunId &&
|
|
343
|
+
manifest.terminal_at !== null &&
|
|
344
|
+
toIsoString(manifest.terminal_at) === input.abandonedAt
|
|
345
|
+
) {
|
|
346
|
+
return sourceRecord(manifest)
|
|
347
|
+
}
|
|
348
|
+
throw sourceConflict(
|
|
349
|
+
`Source materialization '${input.materializationId}' was abandoned by another execution or at another time.`
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
assertSourceCandidateOwner(sourceRecord(manifest), input.execution)
|
|
353
|
+
return this.transitionToAbandoned(sql, manifest, input.abandonedAt)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async abandonForReclaim(
|
|
357
|
+
sql: SQLClient,
|
|
358
|
+
input: ReclaimSourceMaterializationInput
|
|
359
|
+
): Promise<OntologySourceRecord | null> {
|
|
360
|
+
const candidates = await sql<PgOntologySourceRow[]>`
|
|
361
|
+
SELECT * FROM ontology_sources
|
|
362
|
+
WHERE project_id = ${input.projectId}
|
|
363
|
+
AND source_id = ${input.source.projectionId}
|
|
364
|
+
AND projection_run_id = ${input.execution.projectionRunId}
|
|
365
|
+
AND status IN ('staging', 'ready')
|
|
366
|
+
FOR UPDATE
|
|
367
|
+
`
|
|
368
|
+
if (
|
|
369
|
+
candidates.some((candidate) => candidate.execution_token === input.execution.executionToken)
|
|
370
|
+
) {
|
|
371
|
+
throw sourceConflict(
|
|
372
|
+
`Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
if (candidates.length > 1) {
|
|
376
|
+
throw sourceConflict(
|
|
377
|
+
`Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
return candidates[0] ? this.transitionToAbandoned(sql, candidates[0], input.abandonedAt) : null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private async transitionToAbandoned(
|
|
384
|
+
sql: SQLClient,
|
|
385
|
+
manifest: PgOntologySourceRow,
|
|
386
|
+
abandonedAt: string
|
|
387
|
+
): Promise<OntologySourceRecord> {
|
|
388
|
+
if (
|
|
389
|
+
abandonedAt < toIsoString(manifest.created_at) ||
|
|
390
|
+
(manifest.ready_at !== null && abandonedAt < toIsoString(manifest.ready_at))
|
|
391
|
+
) {
|
|
392
|
+
throw new MaterializationValidationError(
|
|
393
|
+
"Source abandonedAt cannot precede source creation or readiness."
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
const rows = await sql<PgOntologySourceRow[]>`
|
|
397
|
+
UPDATE ontology_sources
|
|
398
|
+
SET status = 'abandoned', execution_token = NULL,
|
|
399
|
+
terminal_at = ${abandonedAt}, updated_at = ${abandonedAt}
|
|
400
|
+
WHERE project_id = ${manifest.project_id}
|
|
401
|
+
AND source_id = ${manifest.source_id}
|
|
402
|
+
AND materialization_id = ${manifest.materialization_id}
|
|
403
|
+
AND status IN ('staging', 'ready')
|
|
404
|
+
RETURNING *
|
|
405
|
+
`
|
|
406
|
+
if (!rows[0]) {
|
|
407
|
+
throw sourceConflict(
|
|
408
|
+
`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
return sourceRecord(rows[0])
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async assertReadyState(
|
|
415
|
+
sql: SQLClient,
|
|
416
|
+
manifest: PgOntologySourceRow,
|
|
417
|
+
rootCount: number,
|
|
418
|
+
assertionCount: number
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const [counts] = await sql<
|
|
421
|
+
{
|
|
422
|
+
readonly assertions: number | string
|
|
423
|
+
readonly roots: number | string
|
|
424
|
+
readonly ordinals: number | string
|
|
425
|
+
readonly min_ordinal: number | string | null
|
|
426
|
+
readonly max_ordinal: number | string | null
|
|
427
|
+
}[]
|
|
428
|
+
>`
|
|
429
|
+
SELECT COUNT(*) AS assertions, COUNT(DISTINCT root_key) AS roots,
|
|
430
|
+
COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
431
|
+
MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
|
|
432
|
+
FROM ontology_source_rows
|
|
433
|
+
WHERE project_id = ${manifest.project_id}
|
|
434
|
+
AND source_id = ${manifest.source_id}
|
|
435
|
+
AND materialization_id = ${manifest.materialization_id}
|
|
436
|
+
`
|
|
437
|
+
const assertions = Number(counts?.assertions ?? 0)
|
|
438
|
+
const roots = Number(counts?.roots ?? 0)
|
|
439
|
+
const ordinals = Number(counts?.ordinals ?? 0)
|
|
440
|
+
const minimum = counts?.min_ordinal === null ? null : Number(counts?.min_ordinal)
|
|
441
|
+
const maximum = counts?.max_ordinal === null ? null : Number(counts?.max_ordinal)
|
|
442
|
+
if (
|
|
443
|
+
assertions !== assertionCount ||
|
|
444
|
+
roots !== rootCount ||
|
|
445
|
+
ordinals !== rootCount ||
|
|
446
|
+
(rootCount > 0 && (minimum !== 0 || maximum !== rootCount - 1))
|
|
447
|
+
) {
|
|
448
|
+
throw new MaterializationValidationError(
|
|
449
|
+
"Source ready counts do not match the staged roots and assertions."
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const [invalid] =
|
|
454
|
+
manifest.projection_kind === "link"
|
|
455
|
+
? await sql<{ readonly root_key: unknown }[]>`
|
|
456
|
+
SELECT root_key
|
|
457
|
+
FROM ontology_source_rows
|
|
458
|
+
WHERE project_id = ${manifest.project_id}
|
|
459
|
+
AND source_id = ${manifest.source_id}
|
|
460
|
+
AND materialization_id = ${manifest.materialization_id}
|
|
461
|
+
GROUP BY root_key
|
|
462
|
+
HAVING COUNT(*) <> 1
|
|
463
|
+
OR BOOL_OR(root_kind <> 'link')
|
|
464
|
+
OR BOOL_OR(entity_kind <> 'link')
|
|
465
|
+
OR BOOL_OR(root_key <> entity_key)
|
|
466
|
+
LIMIT 1
|
|
467
|
+
`
|
|
468
|
+
: await sql<{ readonly root_key: unknown }[]>`
|
|
469
|
+
SELECT root_key
|
|
470
|
+
FROM ontology_source_rows
|
|
471
|
+
WHERE project_id = ${manifest.project_id}
|
|
472
|
+
AND source_id = ${manifest.source_id}
|
|
473
|
+
AND materialization_id = ${manifest.materialization_id}
|
|
474
|
+
GROUP BY root_key
|
|
475
|
+
HAVING BOOL_OR(root_kind <> 'object')
|
|
476
|
+
OR COUNT(*) FILTER (
|
|
477
|
+
WHERE entity_kind = 'object' AND entity_key = root_key
|
|
478
|
+
) <> 1
|
|
479
|
+
OR COUNT(*) FILTER (
|
|
480
|
+
WHERE (entity_kind = 'object' AND entity_key <> root_key)
|
|
481
|
+
OR (entity_kind = 'link' AND (
|
|
482
|
+
source_type_id <> root_object_type_id
|
|
483
|
+
OR source_primary_id <> root_primary_id
|
|
484
|
+
))
|
|
485
|
+
) <> 0
|
|
486
|
+
LIMIT 1
|
|
487
|
+
`
|
|
488
|
+
if (invalid) {
|
|
489
|
+
throw new MaterializationValidationError(
|
|
490
|
+
manifest.projection_kind === "link"
|
|
491
|
+
? "Link projection roots must contain exactly their matching link assertion."
|
|
492
|
+
: "Object projection roots must contain exactly their matching object assertion plus links sourced from that root."
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async findStageRows(
|
|
498
|
+
sql: SQLClient,
|
|
499
|
+
input: StageSourceRowsInput,
|
|
500
|
+
rows: readonly SourceStageRow[]
|
|
501
|
+
): Promise<readonly SourceStageRow[]> {
|
|
502
|
+
if (rows.length === 0) return []
|
|
503
|
+
const rootSortKeys = [...new Set(rows.map((row) => utf8SortKey(row.rootKey)))]
|
|
504
|
+
const ordinals = [...new Set(rows.map((row) => row.row.stagingOrdinal))]
|
|
505
|
+
const entitySortKeys = [...new Set(rows.map((row) => utf8SortKey(row.entityKey)))]
|
|
506
|
+
const existing = await sql<PgOntologySourceAssertionRow[]>`
|
|
507
|
+
SELECT * FROM ontology_source_rows
|
|
508
|
+
WHERE project_id = ${input.projectId}
|
|
509
|
+
AND source_id = ${input.source.projectionId}
|
|
510
|
+
AND materialization_id = ${input.materializationId}
|
|
511
|
+
AND (
|
|
512
|
+
root_sort_key = ANY(${sql.array(rootSortKeys)}::text[])
|
|
513
|
+
OR staging_ordinal = ANY(${sql.array(ordinals)}::bigint[])
|
|
514
|
+
OR entity_sort_key = ANY(${sql.array(entitySortKeys)}::text[])
|
|
515
|
+
)
|
|
516
|
+
`
|
|
517
|
+
return existing.map((row) => sourceStageRow(sourceAssertion(row)))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async insertStageRows(
|
|
521
|
+
sql: SQLClient,
|
|
522
|
+
input: StageSourceRowsInput,
|
|
523
|
+
rows: readonly SourceStageRow[]
|
|
524
|
+
): Promise<void> {
|
|
525
|
+
if (rows.length === 0) return
|
|
526
|
+
const payload = rows.map(({ entityKey, rootKey, row }) => {
|
|
527
|
+
const entity = entityColumns(row.assertion)
|
|
528
|
+
const root = entityColumns(row.root)
|
|
529
|
+
return {
|
|
530
|
+
entityKind: row.assertion.kind,
|
|
531
|
+
entityKey: JSON.parse(entityKey) as unknown,
|
|
532
|
+
entitySortKey: utf8SortKey(entityKey),
|
|
533
|
+
rootKind: row.root.kind,
|
|
534
|
+
rootKey: JSON.parse(rootKey) as unknown,
|
|
535
|
+
rootSortKey: utf8SortKey(rootKey),
|
|
536
|
+
stagingOrdinal: row.stagingOrdinal,
|
|
537
|
+
root: row.root,
|
|
538
|
+
assertion: row.assertion,
|
|
539
|
+
objectTypeId: entity.objectTypeId,
|
|
540
|
+
primaryId: entity.primaryId,
|
|
541
|
+
sourceTypeId: entity.sourceTypeId,
|
|
542
|
+
sourcePrimaryId: entity.sourcePrimaryId,
|
|
543
|
+
linkId: entity.linkId,
|
|
544
|
+
targetTypeId: entity.targetTypeId,
|
|
545
|
+
targetPrimaryId: entity.targetPrimaryId,
|
|
546
|
+
rootObjectTypeId: root.objectTypeId,
|
|
547
|
+
rootPrimaryId: root.primaryId,
|
|
548
|
+
rootSourceTypeId: root.sourceTypeId,
|
|
549
|
+
rootSourcePrimaryId: root.sourcePrimaryId,
|
|
550
|
+
rootLinkId: root.linkId,
|
|
551
|
+
rootTargetTypeId: root.targetTypeId,
|
|
552
|
+
rootTargetPrimaryId: root.targetPrimaryId,
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
await sql`
|
|
556
|
+
WITH staged AS (
|
|
557
|
+
SELECT value
|
|
558
|
+
FROM jsonb_array_elements(${jsonParameter(sql, payload)}::jsonb)
|
|
559
|
+
)
|
|
560
|
+
INSERT INTO ontology_source_rows (
|
|
561
|
+
project_id, source_id, materialization_id,
|
|
562
|
+
entity_kind, entity_key, entity_sort_key,
|
|
563
|
+
root_kind, root_key, root_sort_key, staging_ordinal,
|
|
564
|
+
root, assertion,
|
|
565
|
+
object_type_id, primary_id,
|
|
566
|
+
source_type_id, source_primary_id, link_id, target_type_id, target_primary_id,
|
|
567
|
+
root_object_type_id, root_primary_id,
|
|
568
|
+
root_source_type_id, root_source_primary_id, root_link_id,
|
|
569
|
+
root_target_type_id, root_target_primary_id
|
|
570
|
+
)
|
|
571
|
+
SELECT
|
|
572
|
+
${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
|
|
573
|
+
value->>'entityKind', value->'entityKey', value->>'entitySortKey',
|
|
574
|
+
value->>'rootKind', value->'rootKey', value->>'rootSortKey',
|
|
575
|
+
(value->>'stagingOrdinal')::bigint,
|
|
576
|
+
value->'root', value->'assertion',
|
|
577
|
+
value->>'objectTypeId', value->>'primaryId',
|
|
578
|
+
value->>'sourceTypeId', value->>'sourcePrimaryId', value->>'linkId',
|
|
579
|
+
value->>'targetTypeId', value->>'targetPrimaryId',
|
|
580
|
+
value->>'rootObjectTypeId', value->>'rootPrimaryId',
|
|
581
|
+
value->>'rootSourceTypeId', value->>'rootSourcePrimaryId', value->>'rootLinkId',
|
|
582
|
+
value->>'rootTargetTypeId', value->>'rootTargetPrimaryId'
|
|
583
|
+
FROM staged
|
|
584
|
+
`
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private assertExecution(
|
|
588
|
+
sql: SQLClient,
|
|
589
|
+
input: {
|
|
590
|
+
readonly projectId: string
|
|
591
|
+
readonly source: { readonly projectionId: string }
|
|
592
|
+
readonly execution: { readonly projectionRunId: string; readonly executionToken: string }
|
|
593
|
+
},
|
|
594
|
+
identity?: AssertSourceMaterializationExecutionInput["identity"]
|
|
595
|
+
): Promise<void> {
|
|
596
|
+
return assertProjectionExecution(sql, {
|
|
597
|
+
projectId: input.projectId,
|
|
598
|
+
sourceId: input.source.projectionId,
|
|
599
|
+
projectionRunId: input.execution.projectionRunId,
|
|
600
|
+
executionToken: input.execution.executionToken,
|
|
601
|
+
...(identity ? { identity } : {}),
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private async getRunCandidate(
|
|
606
|
+
sql: SQLClient,
|
|
607
|
+
input: BeginSourceMaterializationInput
|
|
608
|
+
): Promise<PgOntologySourceRow | null> {
|
|
609
|
+
const [row] = await sql<PgOntologySourceRow[]>`
|
|
610
|
+
SELECT * FROM ontology_sources
|
|
611
|
+
WHERE project_id = ${input.projectId}
|
|
612
|
+
AND projection_run_id = ${input.execution.projectionRunId}
|
|
613
|
+
AND status IN ('staging', 'ready')
|
|
614
|
+
LIMIT 1
|
|
615
|
+
FOR UPDATE
|
|
616
|
+
`
|
|
617
|
+
return row ?? null
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async getManifest(
|
|
621
|
+
sql: SQLClient,
|
|
622
|
+
projectId: string,
|
|
623
|
+
sourceId: string,
|
|
624
|
+
materializationId: string,
|
|
625
|
+
lock = false
|
|
626
|
+
): Promise<PgOntologySourceRow | null> {
|
|
627
|
+
const lockFragment = lock ? sql`FOR UPDATE` : sql``
|
|
628
|
+
const [row] = await sql<PgOntologySourceRow[]>`
|
|
629
|
+
SELECT * FROM ontology_sources
|
|
630
|
+
WHERE project_id = ${projectId}
|
|
631
|
+
AND source_id = ${sourceId}
|
|
632
|
+
AND materialization_id = ${materializationId}
|
|
633
|
+
${lockFragment}
|
|
634
|
+
`
|
|
635
|
+
return row ?? null
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private async requireManifest(
|
|
639
|
+
sql: SQLClient,
|
|
640
|
+
projectId: string,
|
|
641
|
+
sourceId: string,
|
|
642
|
+
materializationId: string,
|
|
643
|
+
lock = false
|
|
644
|
+
): Promise<PgOntologySourceRow> {
|
|
645
|
+
const row = await this.getManifest(sql, projectId, sourceId, materializationId, lock)
|
|
646
|
+
if (!row) throw sourceConflict(`Source materialization '${materializationId}' does not exist.`)
|
|
647
|
+
return row
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function toOptionalIsoString(value: Date | string | null | undefined): string | null {
|
|
652
|
+
if (value === null || value === undefined) return null
|
|
653
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function numberOrNull(value: number | string | null): number | null {
|
|
657
|
+
return value === null ? null : Number(value)
|
|
658
|
+
}
|