@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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. 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
+ }