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