@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,939 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EffectiveChangeCounts,
|
|
3
|
+
ExpectedLinkRevision,
|
|
4
|
+
ExpectedObjectRevision,
|
|
5
|
+
} from "@sixb/core/internal/materialization"
|
|
6
|
+
import {
|
|
7
|
+
assertPinnedDatasetWatermark,
|
|
8
|
+
linkRefKey,
|
|
9
|
+
linkRefSortKey,
|
|
10
|
+
linkScopeSortKey,
|
|
11
|
+
MaterializationConflictError,
|
|
12
|
+
objectRefKey,
|
|
13
|
+
objectRefSortKey,
|
|
14
|
+
telemetryPointKey,
|
|
15
|
+
telemetryPointSortKey,
|
|
16
|
+
} from "@sixb/core/internal/materialization"
|
|
17
|
+
import {
|
|
18
|
+
assertMaterializationFinalizationCorrelation,
|
|
19
|
+
assertMaterializationHeader,
|
|
20
|
+
assertMaterializationLaneCompletion,
|
|
21
|
+
assertPageRows,
|
|
22
|
+
assertPlanChunkCorrelations,
|
|
23
|
+
assertSourceActivationCorrelation,
|
|
24
|
+
invalidCorrelation,
|
|
25
|
+
sameNonnegativeCounts as sameCounts,
|
|
26
|
+
uniqueSorted,
|
|
27
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
28
|
+
import type {
|
|
29
|
+
ApplyMaterializationChunkInput,
|
|
30
|
+
ApplyMaterializationResult,
|
|
31
|
+
FinalizeMaterializationInput,
|
|
32
|
+
MaterializationObjectExistence,
|
|
33
|
+
MaterializationPlanHeader,
|
|
34
|
+
MaterializationSession,
|
|
35
|
+
MaterializationStatePage,
|
|
36
|
+
MaterializationWorkPage,
|
|
37
|
+
OntologyCommitRecord,
|
|
38
|
+
OntologyMaterializationStorage,
|
|
39
|
+
ReadMaterializationObjectExistenceInput,
|
|
40
|
+
SourceActivationWrite,
|
|
41
|
+
SourceReplacementStatePage,
|
|
42
|
+
StageMaterializationWorkInput,
|
|
43
|
+
StreamMaterializationStateInput,
|
|
44
|
+
StreamMaterializationWorkInput,
|
|
45
|
+
StreamSourceReplacementStateInput,
|
|
46
|
+
} from "@sixb/core/storage"
|
|
47
|
+
import type { SQLClient } from "../pg-client"
|
|
48
|
+
import { isUniqueViolation } from "../storage-errors"
|
|
49
|
+
import { lockAdvisoryKeys } from "../transactions"
|
|
50
|
+
import {
|
|
51
|
+
type PgMaterializationSessionState,
|
|
52
|
+
PgMaterializationSessions,
|
|
53
|
+
type PgOntologyTransactionContext,
|
|
54
|
+
} from "./materialization-session"
|
|
55
|
+
import {
|
|
56
|
+
linkSortExpression,
|
|
57
|
+
PG_MATERIALIZATION_WORK_TABLE,
|
|
58
|
+
PgMaterializationStateReader,
|
|
59
|
+
} from "./materialization-state"
|
|
60
|
+
import { PgMaterializationWriter } from "./materialization-writer"
|
|
61
|
+
import {
|
|
62
|
+
assertProjectionExecution,
|
|
63
|
+
commitRecord,
|
|
64
|
+
jsonParameter,
|
|
65
|
+
ontologyLockKey,
|
|
66
|
+
originColumns,
|
|
67
|
+
type PgOntologyCommitRow,
|
|
68
|
+
type PgOntologySourceRow,
|
|
69
|
+
toIsoString,
|
|
70
|
+
} from "./shared"
|
|
71
|
+
|
|
72
|
+
interface ProjectionCountsRow {
|
|
73
|
+
readonly object_classifications: number | string
|
|
74
|
+
readonly link_classifications: number | string
|
|
75
|
+
readonly objects_created: number | string
|
|
76
|
+
readonly objects_updated: number | string
|
|
77
|
+
readonly objects_deleted: number | string
|
|
78
|
+
readonly links_created: number | string
|
|
79
|
+
readonly links_updated: number | string
|
|
80
|
+
readonly links_deleted: number | string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface TelemetrySummaryRow {
|
|
84
|
+
readonly classified_points: number | string
|
|
85
|
+
readonly points_created: number | string
|
|
86
|
+
readonly points_updated: number | string
|
|
87
|
+
readonly latest_objects_changed: number | string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class PgOntologyMaterializationStorage implements OntologyMaterializationStorage {
|
|
91
|
+
private readonly sessions: PgMaterializationSessions
|
|
92
|
+
private readonly writer: PgMaterializationWriter
|
|
93
|
+
|
|
94
|
+
constructor(
|
|
95
|
+
private readonly sql: SQLClient,
|
|
96
|
+
context: PgOntologyTransactionContext | null
|
|
97
|
+
) {
|
|
98
|
+
this.sessions = new PgMaterializationSessions(sql, context)
|
|
99
|
+
this.writer = new PgMaterializationWriter(sql)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async begin(input: MaterializationPlanHeader): Promise<MaterializationSession> {
|
|
103
|
+
assertMaterializationHeader(input)
|
|
104
|
+
const session = await this.sessions.create(input)
|
|
105
|
+
try {
|
|
106
|
+
await lockAdvisoryKeys(this.sql, materializationLockKeys(input))
|
|
107
|
+
await this.assertCommitAbsent(input)
|
|
108
|
+
const reader = new PgMaterializationStateReader(this.sql, input.commit.projectId)
|
|
109
|
+
await this.assertSources(input.expected.sources, input.commit.projectId)
|
|
110
|
+
const objectRevisions = await reader.effectiveObjectRevisions(
|
|
111
|
+
input.expected.objects.map((expected) => expected.ref),
|
|
112
|
+
true
|
|
113
|
+
)
|
|
114
|
+
for (const expected of input.expected.objects) {
|
|
115
|
+
this.assertObject(objectRevisions.get(objectRefKey(expected.ref)) ?? null, expected)
|
|
116
|
+
}
|
|
117
|
+
const linkRevisions = await reader.effectiveLinkLastCommits(
|
|
118
|
+
input.expected.links.map((expected) => expected.ref),
|
|
119
|
+
true
|
|
120
|
+
)
|
|
121
|
+
for (const expected of input.expected.links) {
|
|
122
|
+
this.assertLink(linkRevisions.get(linkRefKey(expected.ref)), expected)
|
|
123
|
+
}
|
|
124
|
+
const linkScopes = await reader.linkScopes(input.expected.linkScopes)
|
|
125
|
+
for (const [index, expected] of input.expected.linkScopes.entries()) {
|
|
126
|
+
if (linkScopes[index]?.fingerprint !== expected.fingerprint) {
|
|
127
|
+
throw new MaterializationConflictError(
|
|
128
|
+
"effective-state",
|
|
129
|
+
`Expected link scope changed for ${expected.source.objectTypeId}:${expected.source.primaryId}.${expected.linkId}.`
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const points = await reader.exactPoints(input.expected.points, true)
|
|
134
|
+
const pointRevisions = new Map(
|
|
135
|
+
points.map(
|
|
136
|
+
(point) => [telemetryPointKey(point.series, point.at), point.lastCommitId] as const
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
for (const expected of input.expected.points) {
|
|
140
|
+
if (
|
|
141
|
+
(pointRevisions.get(telemetryPointKey(expected.series, expected.at)) ?? null) !==
|
|
142
|
+
expected.lastCommitId
|
|
143
|
+
) {
|
|
144
|
+
throw new MaterializationConflictError(
|
|
145
|
+
"timeseries-point",
|
|
146
|
+
`Telemetry point ${telemetryPointKey(expected.series, expected.at)} changed.`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return session.publicSession()
|
|
151
|
+
} catch (error) {
|
|
152
|
+
await this.sessions.release(session)
|
|
153
|
+
throw error
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async *streamState(
|
|
158
|
+
input: StreamMaterializationStateInput
|
|
159
|
+
): AsyncIterable<MaterializationStatePage> {
|
|
160
|
+
const session = this.sessions.require(input.session)
|
|
161
|
+
assertPageRows(input.pageRows)
|
|
162
|
+
const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId)
|
|
163
|
+
for await (const request of input.requests) {
|
|
164
|
+
this.sessions.require(input.session)
|
|
165
|
+
const objects = uniqueSorted(request.objects, objectRefKey, objectRefSortKey)
|
|
166
|
+
for (let offset = 0; offset < objects.length; offset += input.pageRows) {
|
|
167
|
+
const states = await reader.objectStates(objects.slice(offset, offset + input.pageRows))
|
|
168
|
+
yield { objects: states, links: [], linkScopes: [], points: [] }
|
|
169
|
+
}
|
|
170
|
+
const links = uniqueSorted(request.links, linkRefKey, linkRefSortKey)
|
|
171
|
+
for (let offset = 0; offset < links.length; offset += input.pageRows) {
|
|
172
|
+
const states = await reader.linkStates(links.slice(offset, offset + input.pageRows))
|
|
173
|
+
yield { objects: [], links: states, linkScopes: [], points: [] }
|
|
174
|
+
}
|
|
175
|
+
for await (const refs of reader.incidentLinks(request.incidentObjects, input.pageRows)) {
|
|
176
|
+
this.sessions.require(input.session)
|
|
177
|
+
yield {
|
|
178
|
+
objects: [],
|
|
179
|
+
links: await reader.linkStates(refs),
|
|
180
|
+
linkScopes: [],
|
|
181
|
+
points: [],
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const scopes = uniqueSorted(
|
|
185
|
+
request.linkScopes,
|
|
186
|
+
(scope) =>
|
|
187
|
+
JSON.stringify([scope.source.objectTypeId, scope.source.primaryId, scope.linkId]),
|
|
188
|
+
(scope) => linkScopeSortKey(scope.source, scope.linkId)
|
|
189
|
+
)
|
|
190
|
+
for (let offset = 0; offset < scopes.length; offset += input.pageRows) {
|
|
191
|
+
yield {
|
|
192
|
+
objects: [],
|
|
193
|
+
links: [],
|
|
194
|
+
linkScopes: await reader.linkScopes(scopes.slice(offset, offset + input.pageRows)),
|
|
195
|
+
points: [],
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const points = uniqueSorted(
|
|
199
|
+
request.points,
|
|
200
|
+
(point) => telemetryPointKey(point.series, point.at),
|
|
201
|
+
(point) => telemetryPointSortKey(point.series, point.at)
|
|
202
|
+
)
|
|
203
|
+
for (let offset = 0; offset < points.length; offset += input.pageRows) {
|
|
204
|
+
const stored = await reader.exactPoints(points.slice(offset, offset + input.pageRows))
|
|
205
|
+
if (stored.length > 0) {
|
|
206
|
+
yield { objects: [], links: [], linkScopes: [], points: stored }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async *streamSourceReplacementState(
|
|
213
|
+
input: StreamSourceReplacementStateInput
|
|
214
|
+
): AsyncIterable<SourceReplacementStatePage> {
|
|
215
|
+
const session = this.sessions.require(input.session)
|
|
216
|
+
assertPageRows(input.pageRows)
|
|
217
|
+
const replacement = await this.requireReplacement(session, input)
|
|
218
|
+
const reader = new PgMaterializationStateReader(this.sql, session.header.commit.projectId)
|
|
219
|
+
if (input.entityKind === "object") {
|
|
220
|
+
if (replacement.projectionKind !== "object") {
|
|
221
|
+
throw new MaterializationConflictError(
|
|
222
|
+
"source-materialization",
|
|
223
|
+
"Link projection replacement cannot stream object state."
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
if (replacement.objectStreamStarted) {
|
|
227
|
+
throw new MaterializationConflictError(
|
|
228
|
+
"effective-state",
|
|
229
|
+
"Replacement object state may only be streamed once per session."
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
replacement.objectStreamStarted = true
|
|
233
|
+
for await (const identities of reader.replacementIdentities({
|
|
234
|
+
sessionId: session.id,
|
|
235
|
+
sourceId: replacement.sourceId,
|
|
236
|
+
candidateMaterializationId: replacement.candidateMaterializationId,
|
|
237
|
+
previousMaterializationId: replacement.previousMaterializationId,
|
|
238
|
+
kind: "object",
|
|
239
|
+
pageRows: input.pageRows,
|
|
240
|
+
})) {
|
|
241
|
+
this.sessions.require(input.session)
|
|
242
|
+
const refs = identities.map((identity) => {
|
|
243
|
+
if (identity.kind !== "object") {
|
|
244
|
+
invalidCorrelation("Object replacement returned a link identity.")
|
|
245
|
+
}
|
|
246
|
+
return identity.ref
|
|
247
|
+
})
|
|
248
|
+
const objects = await reader.replacementObjectStates(
|
|
249
|
+
replacement.sourceId,
|
|
250
|
+
replacement.candidateMaterializationId,
|
|
251
|
+
refs
|
|
252
|
+
)
|
|
253
|
+
yield { objects, links: [] }
|
|
254
|
+
}
|
|
255
|
+
this.sessions.require(input.session)
|
|
256
|
+
replacement.objectStreamCompleted = true
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (replacement.projectionKind === "object" && !replacement.objectStreamCompleted) {
|
|
261
|
+
throw new MaterializationConflictError(
|
|
262
|
+
"effective-state",
|
|
263
|
+
"Object projection replacement must fully stream object state before link state."
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
if (replacement.linkStreamStarted) {
|
|
267
|
+
throw new MaterializationConflictError(
|
|
268
|
+
"effective-state",
|
|
269
|
+
"Replacement link state may only be streamed once per session."
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
replacement.linkStreamStarted = true
|
|
273
|
+
const materializationIds = [
|
|
274
|
+
replacement.candidateMaterializationId,
|
|
275
|
+
...(replacement.previousMaterializationId ? [replacement.previousMaterializationId] : []),
|
|
276
|
+
]
|
|
277
|
+
for await (const identities of reader.replacementIdentities({
|
|
278
|
+
sessionId: session.id,
|
|
279
|
+
sourceId: replacement.sourceId,
|
|
280
|
+
candidateMaterializationId: replacement.candidateMaterializationId,
|
|
281
|
+
previousMaterializationId: replacement.previousMaterializationId,
|
|
282
|
+
kind: "link",
|
|
283
|
+
pageRows: input.pageRows,
|
|
284
|
+
})) {
|
|
285
|
+
this.sessions.require(input.session)
|
|
286
|
+
const linkIdentities = identities.map((identity) => {
|
|
287
|
+
if (identity.kind !== "link") {
|
|
288
|
+
invalidCorrelation("Link replacement returned an object identity.")
|
|
289
|
+
}
|
|
290
|
+
return identity
|
|
291
|
+
})
|
|
292
|
+
const links = await reader.replacementLinkStates(
|
|
293
|
+
replacement.sourceId,
|
|
294
|
+
replacement.candidateMaterializationId,
|
|
295
|
+
materializationIds,
|
|
296
|
+
linkIdentities
|
|
297
|
+
)
|
|
298
|
+
yield { objects: [], links }
|
|
299
|
+
}
|
|
300
|
+
this.sessions.require(input.session)
|
|
301
|
+
replacement.linkStreamCompleted = true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async stageWork(input: StageMaterializationWorkInput): Promise<void> {
|
|
305
|
+
await this.sessions.stage(input)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
streamWork(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
|
|
309
|
+
assertPageRows(input.pageRows)
|
|
310
|
+
return this.sessions.stream(input)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async readObjectExistence(
|
|
314
|
+
input: ReadMaterializationObjectExistenceInput
|
|
315
|
+
): Promise<readonly MaterializationObjectExistence[]> {
|
|
316
|
+
const session = this.sessions.require(input.session)
|
|
317
|
+
return this.sessions.readObjectExistence(session, input.refs)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async applyChunk(input: ApplyMaterializationChunkInput): Promise<void> {
|
|
321
|
+
const session = this.sessions.require(input.session)
|
|
322
|
+
const { commit } = session.header
|
|
323
|
+
assertPlanChunkCorrelations(input.chunk, commit)
|
|
324
|
+
const progress = await this.sessions.prepareChunkSequence(session, input.chunk)
|
|
325
|
+
await this.writer.apply(commit.projectId, commit.id, input.chunk)
|
|
326
|
+
this.sessions.commitChunkSequence(session, progress)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async finalize(input: FinalizeMaterializationInput): Promise<ApplyMaterializationResult> {
|
|
330
|
+
const session = this.sessions.require(input.session)
|
|
331
|
+
await this.assertCommitAbsent(session.header)
|
|
332
|
+
await this.assertFinalization(session, input)
|
|
333
|
+
for (const activation of input.finalization.sourceActivations) {
|
|
334
|
+
await this.activateSource(session, activation)
|
|
335
|
+
}
|
|
336
|
+
const record = await this.insertCommit(session.header, input)
|
|
337
|
+
await this.sessions.release(session)
|
|
338
|
+
return { commit: record }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
deactivateSessions(): void {
|
|
342
|
+
this.sessions.deactivateAll()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async requireReplacement(
|
|
346
|
+
session: PgMaterializationSessionState,
|
|
347
|
+
input: StreamSourceReplacementStateInput
|
|
348
|
+
): Promise<NonNullable<PgMaterializationSessionState["replacement"]>> {
|
|
349
|
+
if (session.replacement) {
|
|
350
|
+
if (
|
|
351
|
+
session.replacement.sourceId !== input.source.projectionId ||
|
|
352
|
+
session.replacement.candidateMaterializationId !== input.candidateMaterializationId
|
|
353
|
+
) {
|
|
354
|
+
throw new MaterializationConflictError(
|
|
355
|
+
"source-materialization",
|
|
356
|
+
"Materialization session already owns another replacement union."
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
return session.replacement
|
|
360
|
+
}
|
|
361
|
+
const projectId = session.header.commit.projectId
|
|
362
|
+
const candidate = await this.getSource(
|
|
363
|
+
projectId,
|
|
364
|
+
input.source.projectionId,
|
|
365
|
+
input.candidateMaterializationId,
|
|
366
|
+
true
|
|
367
|
+
)
|
|
368
|
+
if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
|
|
369
|
+
throw new MaterializationConflictError(
|
|
370
|
+
"source-materialization",
|
|
371
|
+
`Candidate source materialization '${input.candidateMaterializationId}' is missing or is not ready.`
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
await assertProjectionExecution(this.sql, {
|
|
375
|
+
projectId,
|
|
376
|
+
sourceId: input.source.projectionId,
|
|
377
|
+
projectionRunId: candidate.projection_run_id,
|
|
378
|
+
executionToken: candidate.execution_token,
|
|
379
|
+
})
|
|
380
|
+
const previous = await this.getActiveSource(projectId, input.source.projectionId, true)
|
|
381
|
+
if (
|
|
382
|
+
candidate.protocol !== "replacement" ||
|
|
383
|
+
(previous &&
|
|
384
|
+
(previous.protocol !== candidate.protocol ||
|
|
385
|
+
previous.projection_kind !== candidate.projection_kind))
|
|
386
|
+
) {
|
|
387
|
+
throw new MaterializationConflictError(
|
|
388
|
+
"source-materialization",
|
|
389
|
+
"Source replacement kind or protocol does not match its active materialization."
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
session.replacement = {
|
|
393
|
+
sourceId: input.source.projectionId,
|
|
394
|
+
candidateMaterializationId: input.candidateMaterializationId,
|
|
395
|
+
previousMaterializationId: previous?.materialization_id ?? null,
|
|
396
|
+
projectionKind: candidate.projection_kind,
|
|
397
|
+
objectStreamStarted: false,
|
|
398
|
+
objectStreamCompleted: false,
|
|
399
|
+
linkStreamStarted: false,
|
|
400
|
+
linkStreamCompleted: false,
|
|
401
|
+
}
|
|
402
|
+
return session.replacement
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private async assertCommitAbsent(header: MaterializationPlanHeader): Promise<void> {
|
|
406
|
+
const [duplicateIdentity] = await this.sql<
|
|
407
|
+
{ readonly id: string; readonly idempotency_key: string }[]
|
|
408
|
+
>`
|
|
409
|
+
SELECT id, idempotency_key
|
|
410
|
+
FROM ontology_commits
|
|
411
|
+
WHERE project_id = ${header.commit.projectId}
|
|
412
|
+
AND (id = ${header.commit.id} OR idempotency_key = ${header.commit.idempotencyKey})
|
|
413
|
+
ORDER BY CASE WHEN id = ${header.commit.id} THEN 0 ELSE 1 END
|
|
414
|
+
LIMIT 1
|
|
415
|
+
FOR UPDATE
|
|
416
|
+
`
|
|
417
|
+
if (duplicateIdentity) {
|
|
418
|
+
throw new MaterializationConflictError(
|
|
419
|
+
"idempotency",
|
|
420
|
+
duplicateIdentity.id === header.commit.id
|
|
421
|
+
? `Ontology commit '${header.commit.id}' already exists.`
|
|
422
|
+
: "Ontology idempotency key already exists."
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const origin = originColumns(header.commit.origin)
|
|
427
|
+
if (origin.runId === null) return
|
|
428
|
+
const [duplicateOrigin] =
|
|
429
|
+
origin.batchOrdinal === null
|
|
430
|
+
? await this.sql<{ readonly id: string }[]>`
|
|
431
|
+
SELECT id FROM ontology_commits
|
|
432
|
+
WHERE project_id = ${header.commit.projectId}
|
|
433
|
+
AND origin_kind = ${origin.kind}
|
|
434
|
+
AND origin_run_id = ${origin.runId}
|
|
435
|
+
AND origin_batch_ordinal IS NULL
|
|
436
|
+
LIMIT 1
|
|
437
|
+
FOR UPDATE
|
|
438
|
+
`
|
|
439
|
+
: await this.sql<{ readonly id: string }[]>`
|
|
440
|
+
SELECT id FROM ontology_commits
|
|
441
|
+
WHERE project_id = ${header.commit.projectId}
|
|
442
|
+
AND origin_kind = ${origin.kind}
|
|
443
|
+
AND origin_run_id = ${origin.runId}
|
|
444
|
+
AND origin_batch_ordinal = ${origin.batchOrdinal}
|
|
445
|
+
LIMIT 1
|
|
446
|
+
FOR UPDATE
|
|
447
|
+
`
|
|
448
|
+
if (duplicateOrigin) {
|
|
449
|
+
throw new MaterializationConflictError(
|
|
450
|
+
"run-correlation",
|
|
451
|
+
"Ontology commit origin already has an authoritative commit."
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private async assertSources(
|
|
457
|
+
expectedSources: MaterializationPlanHeader["expected"]["sources"],
|
|
458
|
+
projectId: string
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
if (expectedSources.length === 0) return
|
|
461
|
+
const rows = await this.sql<PgOntologySourceRow[]>`
|
|
462
|
+
SELECT * FROM ontology_sources
|
|
463
|
+
WHERE project_id = ${projectId}
|
|
464
|
+
AND source_id = ANY(
|
|
465
|
+
${this.sql.array(expectedSources.map((expected) => expected.source.projectionId))}::text[]
|
|
466
|
+
)
|
|
467
|
+
AND status = 'active'
|
|
468
|
+
FOR UPDATE
|
|
469
|
+
`
|
|
470
|
+
const active = new Map(rows.map((row) => [row.source_id, row] as const))
|
|
471
|
+
for (const expected of expectedSources) {
|
|
472
|
+
this.assertSourceRow(expected, active.get(expected.source.projectionId) ?? null)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private assertSourceRow(
|
|
477
|
+
expected: MaterializationPlanHeader["expected"]["sources"][number],
|
|
478
|
+
active: PgOntologySourceRow | null
|
|
479
|
+
): void {
|
|
480
|
+
if (
|
|
481
|
+
(active?.materialization_id ?? null) !== expected.activeMaterializationId ||
|
|
482
|
+
(active?.last_commit_id ?? null) !== expected.lastCommitId
|
|
483
|
+
) {
|
|
484
|
+
throw new MaterializationConflictError(
|
|
485
|
+
"projection-fence",
|
|
486
|
+
`Source '${expected.source.projectionId}' changed.`
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private assertObject(
|
|
492
|
+
row: { readonly version: number; readonly lastCommitId: string | null } | null,
|
|
493
|
+
expected: ExpectedObjectRevision
|
|
494
|
+
): void {
|
|
495
|
+
if (!expected.exists) {
|
|
496
|
+
if (row) {
|
|
497
|
+
throw new MaterializationConflictError(
|
|
498
|
+
"effective-state",
|
|
499
|
+
`Expected object ${objectRefKey(expected.ref)} to be absent.`
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
if (!row || row.version !== expected.version || row.lastCommitId !== expected.lastCommitId) {
|
|
505
|
+
throw new MaterializationConflictError(
|
|
506
|
+
"effective-state",
|
|
507
|
+
`Expected object ${objectRefKey(expected.ref)} changed.`
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private assertLink(
|
|
513
|
+
lastCommitId: string | null | undefined,
|
|
514
|
+
expected: ExpectedLinkRevision
|
|
515
|
+
): void {
|
|
516
|
+
if (!expected.exists) {
|
|
517
|
+
if (lastCommitId !== undefined) {
|
|
518
|
+
throw new MaterializationConflictError(
|
|
519
|
+
"effective-state",
|
|
520
|
+
`Expected link ${linkRefKey(expected.ref)} to be absent.`
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
if (lastCommitId === undefined || lastCommitId !== expected.lastCommitId) {
|
|
526
|
+
throw new MaterializationConflictError(
|
|
527
|
+
"effective-state",
|
|
528
|
+
`Expected link ${linkRefKey(expected.ref)} changed.`
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private async assertFinalization(
|
|
534
|
+
session: PgMaterializationSessionState,
|
|
535
|
+
input: FinalizeMaterializationInput
|
|
536
|
+
): Promise<void> {
|
|
537
|
+
const { commit } = session.header
|
|
538
|
+
const { result } = input.finalization
|
|
539
|
+
assertMaterializationFinalizationCorrelation(session, input)
|
|
540
|
+
const laneCounts = await this.sessions.laneCounts(session)
|
|
541
|
+
assertMaterializationLaneCompletion(session, laneCounts)
|
|
542
|
+
await this.assertFinalCardinality(session)
|
|
543
|
+
const eventCount = laneCounts.event
|
|
544
|
+
const [outbox] = await this.sql<
|
|
545
|
+
{
|
|
546
|
+
readonly count: number | string
|
|
547
|
+
readonly minimum: number | string | null
|
|
548
|
+
readonly maximum: number | string | null
|
|
549
|
+
}[]
|
|
550
|
+
>`
|
|
551
|
+
SELECT COUNT(*) AS count, MIN(commit_ordinal) AS minimum,
|
|
552
|
+
MAX(commit_ordinal) AS maximum
|
|
553
|
+
FROM ontology_outbox
|
|
554
|
+
WHERE project_id = ${commit.projectId} AND commit_id = ${commit.id}
|
|
555
|
+
`
|
|
556
|
+
const outboxCount = Number(outbox?.count ?? 0)
|
|
557
|
+
const minimum = outbox?.minimum === null ? null : Number(outbox?.minimum)
|
|
558
|
+
const maximum = outbox?.maximum === null ? null : Number(outbox?.maximum)
|
|
559
|
+
if (
|
|
560
|
+
outboxCount !== eventCount ||
|
|
561
|
+
(eventCount > 0 && (minimum !== 0 || maximum !== eventCount - 1))
|
|
562
|
+
) {
|
|
563
|
+
invalidCorrelation("Outbox event ordinals must be contiguous from zero.")
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (commit.intent.kind === "telemetry") {
|
|
567
|
+
const summary = await this.telemetrySummary(session, commit.intent.pointCount)
|
|
568
|
+
if (summary.classifiedPoints !== commit.intent.pointCount) {
|
|
569
|
+
invalidCorrelation(
|
|
570
|
+
"Telemetry point classification coverage does not match the commit intent."
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
if (result.kind !== "telemetry" || !sameCounts(result, summary.counts)) {
|
|
574
|
+
invalidCorrelation("Telemetry result counts do not correlate with finalized work.")
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (commit.intent.kind === "projection") {
|
|
578
|
+
if (result.kind !== "projection") return
|
|
579
|
+
await this.sessions.assertClassificationCoverage(session)
|
|
580
|
+
if (!sameCounts(result.counts, await this.projectionCounts(session))) {
|
|
581
|
+
invalidCorrelation("Projection result counts do not correlate with finalized work.")
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
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,
|
|
591
|
+
(payload->>'occupied')::boolean AS occupied,
|
|
592
|
+
payload->'ref'->'source'->>'objectTypeId' AS source_type_id,
|
|
593
|
+
payload->'ref'->'source'->>'primaryId' AS source_id,
|
|
594
|
+
payload->'ref'->>'linkId' AS link_id
|
|
595
|
+
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
596
|
+
WHERE session_id = ${session.id} AND kind = 'cardinality'
|
|
597
|
+
), duplicate AS (
|
|
598
|
+
SELECT scope_sort_key FROM work WHERE occupied
|
|
599
|
+
GROUP BY scope_sort_key HAVING COUNT(*) > 1
|
|
600
|
+
), scopes AS (
|
|
601
|
+
SELECT DISTINCT scope_sort_key, source_type_id, source_id, link_id FROM work
|
|
602
|
+
), expected AS (
|
|
603
|
+
SELECT scope_sort_key, link_sort_key FROM work WHERE occupied
|
|
604
|
+
), actual AS (
|
|
605
|
+
SELECT scopes.scope_sort_key,
|
|
606
|
+
${this.sql.unsafe(linkSortExpression("links"))} AS link_sort_key
|
|
607
|
+
FROM scopes
|
|
608
|
+
JOIN links USING (source_type_id, source_id, link_id)
|
|
609
|
+
WHERE links.project_id = ${session.header.commit.projectId}
|
|
610
|
+
), differences AS (
|
|
611
|
+
(SELECT * FROM expected EXCEPT SELECT * FROM actual)
|
|
612
|
+
UNION ALL
|
|
613
|
+
(SELECT * FROM actual EXCEPT SELECT * FROM expected)
|
|
614
|
+
)
|
|
615
|
+
SELECT 'duplicate'::text AS reason FROM duplicate
|
|
616
|
+
UNION ALL
|
|
617
|
+
SELECT 'mismatch'::text AS reason FROM differences
|
|
618
|
+
LIMIT 1
|
|
619
|
+
`
|
|
620
|
+
if (violation?.reason === "duplicate") {
|
|
621
|
+
invalidCorrelation("Materialization cardinality work violates cardinality-one.")
|
|
622
|
+
}
|
|
623
|
+
if (violation) {
|
|
624
|
+
invalidCorrelation(
|
|
625
|
+
"Materialization cardinality work does not match the final effective link scope."
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private async projectionCounts(
|
|
631
|
+
session: PgMaterializationSessionState
|
|
632
|
+
): Promise<EffectiveChangeCounts> {
|
|
633
|
+
const [row] = await this.sql<ProjectionCountsRow[]>`
|
|
634
|
+
SELECT
|
|
635
|
+
COUNT(*) FILTER (
|
|
636
|
+
WHERE kind = 'classification' AND payload->>'entityKind' = 'object'
|
|
637
|
+
) AS object_classifications,
|
|
638
|
+
COUNT(*) FILTER (
|
|
639
|
+
WHERE kind = 'classification' AND payload->>'entityKind' = 'link'
|
|
640
|
+
) AS link_classifications,
|
|
641
|
+
COUNT(*) FILTER (
|
|
642
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
|
|
643
|
+
AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
644
|
+
) AS objects_created,
|
|
645
|
+
COUNT(*) FILTER (
|
|
646
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
|
|
647
|
+
AND (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
648
|
+
) AS objects_updated,
|
|
649
|
+
COUNT(*) FILTER (
|
|
650
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-delete'
|
|
651
|
+
) AS objects_deleted,
|
|
652
|
+
COUNT(*) FILTER (
|
|
653
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
|
|
654
|
+
AND NOT (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
655
|
+
) AS links_created,
|
|
656
|
+
COUNT(*) FILTER (
|
|
657
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-upsert'
|
|
658
|
+
AND (payload->'item'->'value'->'expected'->>'exists')::boolean
|
|
659
|
+
) AS links_updated,
|
|
660
|
+
COUNT(*) FILTER (
|
|
661
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'link-delete'
|
|
662
|
+
) AS links_deleted
|
|
663
|
+
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
664
|
+
WHERE session_id = ${session.id}
|
|
665
|
+
`
|
|
666
|
+
const objectsCreated = databaseCount(row?.objects_created)
|
|
667
|
+
const objectsUpdated = databaseCount(row?.objects_updated)
|
|
668
|
+
const objectsDeleted = databaseCount(row?.objects_deleted)
|
|
669
|
+
const linksCreated = databaseCount(row?.links_created)
|
|
670
|
+
const linksUpdated = databaseCount(row?.links_updated)
|
|
671
|
+
const linksDeleted = databaseCount(row?.links_deleted)
|
|
672
|
+
return {
|
|
673
|
+
objectsCreated,
|
|
674
|
+
objectsUpdated,
|
|
675
|
+
objectsDeleted,
|
|
676
|
+
objectsUnchanged:
|
|
677
|
+
databaseCount(row?.object_classifications) -
|
|
678
|
+
objectsCreated -
|
|
679
|
+
objectsUpdated -
|
|
680
|
+
objectsDeleted,
|
|
681
|
+
linksCreated,
|
|
682
|
+
linksUpdated,
|
|
683
|
+
linksDeleted,
|
|
684
|
+
linksUnchanged:
|
|
685
|
+
databaseCount(row?.link_classifications) - linksCreated - linksUpdated - linksDeleted,
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private async telemetrySummary(session: PgMaterializationSessionState, pointCount: number) {
|
|
690
|
+
const [row] = await this.sql<TelemetrySummaryRow[]>`
|
|
691
|
+
SELECT
|
|
692
|
+
COUNT(*) FILTER (
|
|
693
|
+
WHERE kind = 'classification' AND payload->>'entityKind' = 'point'
|
|
694
|
+
) AS classified_points,
|
|
695
|
+
COUNT(*) FILTER (
|
|
696
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'point-upsert'
|
|
697
|
+
AND payload->'item'->'value'->'expected'->>'lastCommitId' IS NULL
|
|
698
|
+
) AS points_created,
|
|
699
|
+
COUNT(*) FILTER (
|
|
700
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'point-upsert'
|
|
701
|
+
AND payload->'item'->'value'->'expected'->>'lastCommitId' IS NOT NULL
|
|
702
|
+
) AS points_updated,
|
|
703
|
+
COUNT(*) FILTER (
|
|
704
|
+
WHERE kind = 'plan' AND payload->'item'->>'kind' = 'object-upsert'
|
|
705
|
+
) AS latest_objects_changed
|
|
706
|
+
FROM ${this.sql(PG_MATERIALIZATION_WORK_TABLE)}
|
|
707
|
+
WHERE session_id = ${session.id}
|
|
708
|
+
`
|
|
709
|
+
const pointsCreated = databaseCount(row?.points_created)
|
|
710
|
+
const pointsUpdated = databaseCount(row?.points_updated)
|
|
711
|
+
return {
|
|
712
|
+
classifiedPoints: databaseCount(row?.classified_points),
|
|
713
|
+
counts: {
|
|
714
|
+
pointsCreated,
|
|
715
|
+
pointsUpdated,
|
|
716
|
+
pointsUnchanged: pointCount - pointsCreated - pointsUpdated,
|
|
717
|
+
latestObjectsChanged: databaseCount(row?.latest_objects_changed),
|
|
718
|
+
},
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private async activateSource(
|
|
723
|
+
session: PgMaterializationSessionState,
|
|
724
|
+
activation: SourceActivationWrite
|
|
725
|
+
): Promise<void> {
|
|
726
|
+
const { commit } = session.header
|
|
727
|
+
assertSourceActivationCorrelation(session, activation)
|
|
728
|
+
const candidate = await this.getSource(
|
|
729
|
+
commit.projectId,
|
|
730
|
+
activation.source.projectionId,
|
|
731
|
+
activation.materializationId,
|
|
732
|
+
true
|
|
733
|
+
)
|
|
734
|
+
if (!candidate || candidate.status !== "ready" || candidate.execution_token === null) {
|
|
735
|
+
throw new MaterializationConflictError(
|
|
736
|
+
"source-materialization",
|
|
737
|
+
"Source activation candidate is missing or is not ready."
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
await assertProjectionExecution(this.sql, {
|
|
741
|
+
projectId: commit.projectId,
|
|
742
|
+
sourceId: activation.source.projectionId,
|
|
743
|
+
projectionRunId: activation.execution.projectionRunId,
|
|
744
|
+
executionToken: activation.execution.executionToken,
|
|
745
|
+
})
|
|
746
|
+
if (
|
|
747
|
+
candidate.projection_run_id !== activation.execution.projectionRunId ||
|
|
748
|
+
candidate.execution_token !== activation.execution.executionToken ||
|
|
749
|
+
candidate.projection_kind !== activation.projectionKind ||
|
|
750
|
+
candidate.protocol !== activation.protocol ||
|
|
751
|
+
candidate.dataset_id !== activation.datasetVersion.datasetId ||
|
|
752
|
+
candidate.dataset_version_id !== activation.datasetVersion.versionId ||
|
|
753
|
+
toIsoString(candidate.dataset_version_created_at) !== activation.datasetVersion.createdAt ||
|
|
754
|
+
candidate.projection_revision !== activation.projectionRevision ||
|
|
755
|
+
candidate.ownership_hash !== activation.ownershipHash ||
|
|
756
|
+
candidate.ontology_revision !== activation.ontologyRevision ||
|
|
757
|
+
candidate.ready_at === null ||
|
|
758
|
+
activation.updatedAt < toIsoString(candidate.ready_at)
|
|
759
|
+
) {
|
|
760
|
+
invalidCorrelation("Source activation does not match its ready candidate identity.")
|
|
761
|
+
}
|
|
762
|
+
const previous = await this.getActiveSource(
|
|
763
|
+
commit.projectId,
|
|
764
|
+
activation.source.projectionId,
|
|
765
|
+
true
|
|
766
|
+
)
|
|
767
|
+
this.assertSourceRow(activation.expected, previous)
|
|
768
|
+
if (previous) {
|
|
769
|
+
assertPinnedDatasetWatermark(
|
|
770
|
+
{
|
|
771
|
+
datasetId: previous.dataset_id,
|
|
772
|
+
versionId: previous.dataset_version_id,
|
|
773
|
+
createdAt: toIsoString(previous.dataset_version_created_at),
|
|
774
|
+
},
|
|
775
|
+
activation.datasetVersion,
|
|
776
|
+
"Source activation"
|
|
777
|
+
)
|
|
778
|
+
if (activation.updatedAt < toIsoString(previous.updated_at)) {
|
|
779
|
+
invalidCorrelation("Source activation cannot precede the active materialization update.")
|
|
780
|
+
}
|
|
781
|
+
const superseded = await this.sql<{ readonly materialization_id: string }[]>`
|
|
782
|
+
UPDATE ontology_sources
|
|
783
|
+
SET status = 'superseded', execution_token = NULL,
|
|
784
|
+
terminal_at = ${activation.updatedAt}, updated_at = ${activation.updatedAt}
|
|
785
|
+
WHERE project_id = ${commit.projectId}
|
|
786
|
+
AND source_id = ${activation.source.projectionId}
|
|
787
|
+
AND materialization_id = ${previous.materialization_id}
|
|
788
|
+
AND status = 'active'
|
|
789
|
+
AND last_commit_id IS NOT DISTINCT FROM ${activation.expected.lastCommitId}
|
|
790
|
+
RETURNING materialization_id
|
|
791
|
+
`
|
|
792
|
+
if (superseded.length !== 1) {
|
|
793
|
+
throw new MaterializationConflictError(
|
|
794
|
+
"projection-fence",
|
|
795
|
+
`Source '${activation.source.projectionId}' changed.`
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
let activated: readonly { readonly materialization_id: string }[]
|
|
800
|
+
try {
|
|
801
|
+
activated = await this.sql<{ readonly materialization_id: string }[]>`
|
|
802
|
+
UPDATE ontology_sources
|
|
803
|
+
SET status = 'active', execution_token = NULL,
|
|
804
|
+
activated_at = ${activation.updatedAt}, last_commit_id = ${activation.lastCommitId},
|
|
805
|
+
updated_at = ${activation.updatedAt}
|
|
806
|
+
WHERE project_id = ${commit.projectId}
|
|
807
|
+
AND source_id = ${activation.source.projectionId}
|
|
808
|
+
AND materialization_id = ${activation.materializationId}
|
|
809
|
+
AND status = 'ready'
|
|
810
|
+
AND execution_token = ${activation.execution.executionToken}
|
|
811
|
+
RETURNING materialization_id
|
|
812
|
+
`
|
|
813
|
+
} catch (error) {
|
|
814
|
+
if (isUniqueViolation(error)) {
|
|
815
|
+
throw new MaterializationConflictError(
|
|
816
|
+
"projection-fence",
|
|
817
|
+
`Source '${activation.source.projectionId}' changed.`
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
throw error
|
|
821
|
+
}
|
|
822
|
+
if (activated.length !== 1) {
|
|
823
|
+
throw new MaterializationConflictError(
|
|
824
|
+
"source-materialization",
|
|
825
|
+
"Source activation candidate changed."
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private async insertCommit(
|
|
831
|
+
header: MaterializationPlanHeader,
|
|
832
|
+
input: FinalizeMaterializationInput
|
|
833
|
+
): Promise<OntologyCommitRecord> {
|
|
834
|
+
const { commit } = header
|
|
835
|
+
const origin = originColumns(commit.origin)
|
|
836
|
+
const actor = commit.actor === undefined ? null : jsonParameter(this.sql, commit.actor)
|
|
837
|
+
const rows = await this.sql<PgOntologyCommitRow[]>`
|
|
838
|
+
INSERT INTO ontology_commits (
|
|
839
|
+
project_id, id, idempotency_key, request_hash,
|
|
840
|
+
origin_kind, origin_run_id, origin_batch_ordinal, origin, actor,
|
|
841
|
+
ontology_revision, projection_revision, ownership_hash,
|
|
842
|
+
intent, result, committed_at
|
|
843
|
+
) VALUES (
|
|
844
|
+
${commit.projectId}, ${commit.id}, ${commit.idempotencyKey}, ${commit.requestHash},
|
|
845
|
+
${origin.kind}, ${origin.runId}, ${origin.batchOrdinal},
|
|
846
|
+
${jsonParameter(this.sql, commit.origin)}, ${actor}, ${commit.ontologyRevision},
|
|
847
|
+
${commit.projectionRevision ?? null}, ${commit.ownershipHash ?? null},
|
|
848
|
+
${jsonParameter(this.sql, commit.intent)},
|
|
849
|
+
${jsonParameter(this.sql, input.finalization.result)}, ${commit.committedAt}
|
|
850
|
+
)
|
|
851
|
+
ON CONFLICT DO NOTHING
|
|
852
|
+
RETURNING *
|
|
853
|
+
`
|
|
854
|
+
if (!rows[0]) {
|
|
855
|
+
await this.assertCommitAbsent(header)
|
|
856
|
+
throw new MaterializationConflictError(
|
|
857
|
+
"idempotency",
|
|
858
|
+
"Ontology commit identity already exists."
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
return commitRecord(rows[0])
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private async getActiveSource(
|
|
865
|
+
projectId: string,
|
|
866
|
+
sourceId: string,
|
|
867
|
+
lock = false
|
|
868
|
+
): Promise<PgOntologySourceRow | null> {
|
|
869
|
+
const lockFragment = lock ? this.sql`FOR UPDATE` : this.sql``
|
|
870
|
+
const [row] = await this.sql<PgOntologySourceRow[]>`
|
|
871
|
+
SELECT * FROM ontology_sources
|
|
872
|
+
WHERE project_id = ${projectId} AND source_id = ${sourceId} AND status = 'active'
|
|
873
|
+
${lockFragment}
|
|
874
|
+
`
|
|
875
|
+
return row ?? null
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private async getSource(
|
|
879
|
+
projectId: string,
|
|
880
|
+
sourceId: string,
|
|
881
|
+
materializationId: string,
|
|
882
|
+
lock = false
|
|
883
|
+
): Promise<PgOntologySourceRow | null> {
|
|
884
|
+
const lockFragment = lock ? this.sql`FOR UPDATE` : this.sql``
|
|
885
|
+
const [row] = await this.sql<PgOntologySourceRow[]>`
|
|
886
|
+
SELECT * FROM ontology_sources
|
|
887
|
+
WHERE project_id = ${projectId}
|
|
888
|
+
AND source_id = ${sourceId}
|
|
889
|
+
AND materialization_id = ${materializationId}
|
|
890
|
+
${lockFragment}
|
|
891
|
+
`
|
|
892
|
+
return row ?? null
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function materializationLockKeys(header: MaterializationPlanHeader): string[] {
|
|
897
|
+
const { commit, expected } = header
|
|
898
|
+
const origin = originColumns(commit.origin)
|
|
899
|
+
return [
|
|
900
|
+
ontologyLockKey("commit-id", commit.projectId, commit.id),
|
|
901
|
+
ontologyLockKey("commit-idempotency", commit.projectId, commit.idempotencyKey),
|
|
902
|
+
...(origin.runId === null
|
|
903
|
+
? []
|
|
904
|
+
: [
|
|
905
|
+
ontologyLockKey(
|
|
906
|
+
"commit-origin",
|
|
907
|
+
commit.projectId,
|
|
908
|
+
origin.kind,
|
|
909
|
+
origin.runId,
|
|
910
|
+
origin.batchOrdinal === null ? "" : String(origin.batchOrdinal)
|
|
911
|
+
),
|
|
912
|
+
]),
|
|
913
|
+
...expected.sources.map((value) =>
|
|
914
|
+
ontologyLockKey("source", commit.projectId, value.source.projectionId)
|
|
915
|
+
),
|
|
916
|
+
...expected.objects.map((value) =>
|
|
917
|
+
ontologyLockKey("object", commit.projectId, objectRefKey(value.ref))
|
|
918
|
+
),
|
|
919
|
+
...expected.links.map((value) =>
|
|
920
|
+
ontologyLockKey("link", commit.projectId, linkRefKey(value.ref))
|
|
921
|
+
),
|
|
922
|
+
...expected.linkScopes.map((value) =>
|
|
923
|
+
ontologyLockKey(
|
|
924
|
+
"link-scope",
|
|
925
|
+
commit.projectId,
|
|
926
|
+
value.source.objectTypeId,
|
|
927
|
+
value.source.primaryId,
|
|
928
|
+
value.linkId
|
|
929
|
+
)
|
|
930
|
+
),
|
|
931
|
+
...expected.points.map((value) =>
|
|
932
|
+
ontologyLockKey("point", commit.projectId, telemetryPointKey(value.series, value.at))
|
|
933
|
+
),
|
|
934
|
+
]
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function databaseCount(value: number | string | undefined): number {
|
|
938
|
+
return Number(value ?? 0)
|
|
939
|
+
}
|