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