@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,592 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { randomUUID } from "node:crypto"
3
+ import {
4
+ advanceProjectionTelemetry,
5
+ assertGenericProgressDoesNotAdvanceTelemetry,
6
+ assertProjectionMissingTarget,
7
+ assertProjectionRunExecution,
8
+ assertProjectionRunListWindow,
9
+ assertProjectionRunNonEmpty,
10
+ assertProjectionRunRunning,
11
+ assertProjectionRunStartInput,
12
+ createProjectionRunClaim,
13
+ immutableDatasetVersionConflict,
14
+ mergeProjectionRunProgress,
15
+ type PersistedProjectionRunRecord,
16
+ planProjectionRunFinish,
17
+ planProjectionRunReclaim,
18
+ projectionRunNotFound,
19
+ publicProjectionRunRecord,
20
+ requireTelemetryProjectionRun,
21
+ restoreProjectionRun,
22
+ type StoredProjectionRunRecord,
23
+ staleProjectionRunExecution,
24
+ } from "@sixb/core/internal/projection-run-storage-provider"
25
+ import type {
26
+ AdvanceProjectionTelemetryCheckpointInput,
27
+ FinishProjectionRunInput,
28
+ ListLatestProjectionRunsInput,
29
+ ListLatestProjectionRunsResult,
30
+ ListProjectionRunsInput,
31
+ ListProjectionRunsResult,
32
+ LockProjectionRunForMaterializationInput,
33
+ ProjectionKind,
34
+ ProjectionRunClaim,
35
+ ProjectionRunProgress,
36
+ ProjectionRunRecord,
37
+ ProjectionRunStatus,
38
+ ProjectionRunStorage,
39
+ RecordProjectionMissingTargetInput,
40
+ StartOrReclaimProjectionRunInput,
41
+ TelemetryProjectionRunRecord,
42
+ UpdateProjectionRunInput,
43
+ } from "@sixb/core/storage"
44
+ import { ProjectionRunError } from "@sixb/core/storage"
45
+ import { queryLatestRunsByOwnerId } from "./latest-run-query"
46
+ import { installFreshSqliteSchema } from "./migrations"
47
+ import {
48
+ closeSqliteStoreConnection,
49
+ openSqliteStoreConnection,
50
+ runImmediateTransaction,
51
+ type SqliteStoreConnection,
52
+ } from "./transactions"
53
+
54
+ export interface SqliteProjectionRunStorageOptions {
55
+ /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
56
+ path?: string
57
+ /** Internal shared connection used by bundled SqliteStorage. */
58
+ connection?: SqliteStoreConnection
59
+ }
60
+
61
+ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
62
+ private readonly connection: SqliteStoreConnection
63
+ private readonly db: Database
64
+
65
+ constructor(options: SqliteProjectionRunStorageOptions = {}) {
66
+ this.connection = openSqliteStoreConnection(options)
67
+ this.db = this.connection.db
68
+
69
+ if (this.connection.installFreshSchema) {
70
+ installFreshSqliteSchema(this.db)
71
+ }
72
+ }
73
+
74
+ async startOrReclaim(input: StartOrReclaimProjectionRunInput): Promise<ProjectionRunClaim> {
75
+ return runImmediateTransaction(this.db, () => {
76
+ assertProjectionRunStartInput(input)
77
+
78
+ const metadataConflict = this.db
79
+ .query(
80
+ `
81
+ SELECT 1 FROM projection_runs
82
+ WHERE project_id = ? AND id <> ? AND dataset_id = ? AND dataset_version_id = ?
83
+ AND dataset_version_created_at IS NOT NULL
84
+ AND dataset_version_created_at <> ?
85
+ LIMIT 1
86
+ `
87
+ )
88
+ .get(
89
+ input.projectId,
90
+ input.id,
91
+ input.identity.datasetVersion.datasetId,
92
+ input.identity.datasetVersion.versionId,
93
+ input.identity.datasetVersion.createdAt
94
+ )
95
+ if (metadataConflict) {
96
+ throw immutableDatasetVersionConflict(input.identity)
97
+ }
98
+
99
+ const existing = this.findRow(input.projectId, input.id)
100
+ let attempt = 1
101
+ if (existing) {
102
+ const existingRecord = restoreProjectionRunRow(existing)
103
+ attempt = planProjectionRunReclaim(existingRecord, input).attempt
104
+ }
105
+
106
+ const executionToken = createFreshExecutionToken(existing?.execution_token ?? null)
107
+
108
+ if (existing) {
109
+ const result = this.db
110
+ .query(
111
+ `
112
+ UPDATE projection_runs
113
+ SET attempt = ?, execution_token = ?
114
+ WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
115
+ `
116
+ )
117
+ .run(attempt, executionToken, input.projectId, input.id, existing.execution_token)
118
+ if (result.changes !== 1) throw staleProjectionRunExecution(input.id)
119
+ } else {
120
+ try {
121
+ this.db
122
+ .query(
123
+ `
124
+ INSERT INTO projection_runs (
125
+ project_id,
126
+ id,
127
+ projection_id,
128
+ projection_kind,
129
+ materialization_protocol,
130
+ dataset_id,
131
+ dataset_version_id,
132
+ dataset_version_created_at,
133
+ ontology_revision,
134
+ projection_revision,
135
+ ownership_hash,
136
+ object_type_id,
137
+ source_object_type_id,
138
+ target_object_type_id,
139
+ status,
140
+ started_at,
141
+ attempt,
142
+ execution_token,
143
+ fixed_batch_size,
144
+ next_batch_ordinal,
145
+ next_row_offset,
146
+ input_exhausted,
147
+ source_rows_read,
148
+ source_rows_skipped
149
+ ) VALUES (
150
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, 1, ?, ?, ?, ?, ?,
151
+ 0, 0
152
+ )
153
+ `
154
+ )
155
+ .run(
156
+ input.projectId,
157
+ input.id,
158
+ input.identity.projectionId,
159
+ input.identity.projectionKind,
160
+ input.identity.protocol,
161
+ input.identity.datasetVersion.datasetId,
162
+ input.identity.datasetVersion.versionId,
163
+ input.identity.datasetVersion.createdAt,
164
+ input.identity.ontologyRevision,
165
+ input.identity.projectionRevision,
166
+ input.identity.ownershipHash,
167
+ "objectTypeId" in input.target ? input.target.objectTypeId : null,
168
+ "sourceObjectTypeId" in input.target ? input.target.sourceObjectTypeId : null,
169
+ "sourceObjectTypeId" in input.target ? input.target.targetObjectTypeId : null,
170
+ (input.startedAt ?? new Date()).toISOString(),
171
+ executionToken,
172
+ input.fixedBatchSize ?? null,
173
+ input.fixedBatchSize === undefined ? null : 0,
174
+ input.fixedBatchSize === undefined ? null : 0,
175
+ input.fixedBatchSize === undefined ? null : 0
176
+ )
177
+ } catch (error) {
178
+ if (isUniqueConstraintError(error)) {
179
+ throw new ProjectionRunError(
180
+ `[SixbSqlite] Projection run '${input.id}' already exists for project '${input.projectId}'.`
181
+ )
182
+ }
183
+ throw error
184
+ }
185
+ }
186
+
187
+ return projectionRunClaim(this.requireRow(input.projectId, input.id))
188
+ })
189
+ }
190
+
191
+ async lockForMaterialization(
192
+ input: LockProjectionRunForMaterializationInput
193
+ ): Promise<ProjectionRunRecord> {
194
+ return rowToProjectionRunRecord(this.requireMaterializationExecution(input))
195
+ }
196
+
197
+ async update(input: UpdateProjectionRunInput): Promise<ProjectionRunRecord> {
198
+ return runImmediateTransaction(this.db, () => {
199
+ const existingRow = this.requireMaterializationExecution(input)
200
+ const existing = restoreProjectionRunRow(existingRow)
201
+ assertGenericProgressDoesNotAdvanceTelemetry(existing, input.progress)
202
+ const progress = mergeProjectionRunProgress(existing.progress, input.progress)
203
+ const result = this.updateProgress(existingRow, progress, input.executionToken)
204
+ if (result !== 1) throw staleProjectionRunExecution(input.id)
205
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id))
206
+ })
207
+ }
208
+
209
+ async finish(input: FinishProjectionRunInput): Promise<ProjectionRunRecord> {
210
+ return runImmediateTransaction(this.db, () => {
211
+ const existingRow = this.requireMaterializationExecution(input)
212
+ const plan = planProjectionRunFinish(restoreProjectionRunRow(existingRow), input)
213
+ const result = this.db
214
+ .query(
215
+ `
216
+ UPDATE projection_runs
217
+ SET
218
+ status = ?,
219
+ finished_at = ?,
220
+ execution_token = NULL,
221
+ source_rows_read = ?,
222
+ source_rows_skipped = ?,
223
+ input_exhausted = ?,
224
+ error_message = ?
225
+ WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
226
+ `
227
+ )
228
+ .run(
229
+ input.status,
230
+ plan.finishedAt.toISOString(),
231
+ plan.progress.sourceRowsRead,
232
+ plan.progress.sourceRowsSkipped,
233
+ plan.inputExhausted ? 1 : existingRow.input_exhausted,
234
+ plan.errorMessage ?? null,
235
+ input.projectId,
236
+ input.id,
237
+ input.executionToken
238
+ )
239
+ if (result.changes !== 1) throw staleProjectionRunExecution(input.id)
240
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id))
241
+ })
242
+ }
243
+
244
+ async advanceTelemetryCheckpoint(
245
+ input: AdvanceProjectionTelemetryCheckpointInput
246
+ ): Promise<TelemetryProjectionRunRecord> {
247
+ return runImmediateTransaction(this.db, () => {
248
+ const existing = this.requireMaterializationExecution(input)
249
+ const advance = advanceProjectionTelemetry(restoreProjectionRunRow(existing), input)
250
+ const result = this.db
251
+ .query(
252
+ `
253
+ UPDATE projection_runs
254
+ SET
255
+ next_batch_ordinal = ?,
256
+ next_row_offset = ?,
257
+ input_exhausted = ?,
258
+ source_rows_read = ?,
259
+ source_rows_skipped = ?,
260
+ missing_target_object_type_id = NULL,
261
+ missing_target_object_id = NULL,
262
+ missing_target_batch_ordinal = NULL,
263
+ missing_target_first_seen_at = NULL
264
+ WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
265
+ AND next_batch_ordinal = ? AND input_exhausted = 0
266
+ `
267
+ )
268
+ .run(
269
+ advance.checkpoint.nextBatchOrdinal,
270
+ advance.checkpoint.nextRowOffset,
271
+ advance.checkpoint.inputExhausted ? 1 : 0,
272
+ advance.progress.sourceRowsRead,
273
+ advance.progress.sourceRowsSkipped,
274
+ input.projectId,
275
+ input.id,
276
+ input.executionToken,
277
+ input.batchOrdinal
278
+ )
279
+ if (result.changes !== 1) throw staleProjectionRunExecution(input.id)
280
+ return rowToTelemetryProjectionRunRecord(this.requireRow(input.projectId, input.id))
281
+ })
282
+ }
283
+
284
+ async recordMissingTarget(
285
+ input: RecordProjectionMissingTargetInput
286
+ ): Promise<TelemetryProjectionRunRecord> {
287
+ return runImmediateTransaction(this.db, () => {
288
+ const existing = this.requireMaterializationExecution(input)
289
+ const missingTarget = assertProjectionMissingTarget(
290
+ requireTelemetryProjectionRun(restoreProjectionRunRow(existing)),
291
+ input.missingTarget
292
+ )
293
+ const result = this.db
294
+ .query(
295
+ `
296
+ UPDATE projection_runs
297
+ SET
298
+ missing_target_object_type_id = ?,
299
+ missing_target_object_id = ?,
300
+ missing_target_batch_ordinal = ?,
301
+ missing_target_first_seen_at = ?
302
+ WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
303
+ AND next_batch_ordinal = ?
304
+ `
305
+ )
306
+ .run(
307
+ missingTarget.objectTypeId,
308
+ missingTarget.objectId,
309
+ missingTarget.batchOrdinal,
310
+ missingTarget.firstSeenAt.toISOString(),
311
+ input.projectId,
312
+ input.id,
313
+ input.executionToken,
314
+ missingTarget.batchOrdinal
315
+ )
316
+ if (result.changes !== 1) throw staleProjectionRunExecution(input.id)
317
+ return rowToTelemetryProjectionRunRecord(this.requireRow(input.projectId, input.id))
318
+ })
319
+ }
320
+
321
+ async getById(params: { projectId: string; id: string }): Promise<ProjectionRunRecord | null> {
322
+ const row = this.findRow(params.projectId, params.id)
323
+ return row ? rowToProjectionRunRecord(row) : null
324
+ }
325
+
326
+ async list(input: ListProjectionRunsInput): Promise<ListProjectionRunsResult> {
327
+ assertProjectionRunNonEmpty(input.projectId, "projectId")
328
+
329
+ if (input.statuses && input.statuses.length === 0) {
330
+ return { runs: [], hasMore: false, total: 0 }
331
+ }
332
+
333
+ const whereClauses = ["project_id = ?"]
334
+ const args: (string | number)[] = [input.projectId]
335
+
336
+ if (input.projectionId) {
337
+ whereClauses.push("projection_id = ?")
338
+ args.push(input.projectionId)
339
+ }
340
+ if (input.projectionKind) {
341
+ whereClauses.push("projection_kind = ?")
342
+ args.push(input.projectionKind)
343
+ }
344
+ if (input.datasetId) {
345
+ whereClauses.push("dataset_id = ?")
346
+ args.push(input.datasetId)
347
+ }
348
+ if (input.datasetVersionId) {
349
+ whereClauses.push("dataset_version_id = ?")
350
+ args.push(input.datasetVersionId)
351
+ }
352
+ if (input.objectTypeIds) {
353
+ if (input.objectTypeIds.length === 0) {
354
+ return { runs: [], hasMore: false, total: 0 }
355
+ }
356
+ const placeholders = input.objectTypeIds.map(() => "?").join(", ")
357
+ whereClauses.push(
358
+ `(object_type_id IN (${placeholders})` +
359
+ ` OR (source_object_type_id IN (${placeholders})` +
360
+ ` AND target_object_type_id IN (${placeholders})))`
361
+ )
362
+ args.push(...input.objectTypeIds, ...input.objectTypeIds, ...input.objectTypeIds)
363
+ }
364
+ if (input.statuses) {
365
+ whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
366
+ args.push(...input.statuses)
367
+ }
368
+ if (input.startedAfter) {
369
+ whereClauses.push("started_at >= ?")
370
+ args.push(input.startedAfter.toISOString())
371
+ }
372
+ if (input.startedBefore) {
373
+ whereClauses.push("started_at <= ?")
374
+ args.push(input.startedBefore.toISOString())
375
+ }
376
+
377
+ const where = `WHERE ${whereClauses.join(" AND ")}`
378
+ const order = input.order === "asc" ? "ASC" : "DESC"
379
+ const offset = input.offset ?? 0
380
+ const limit = input.limit
381
+
382
+ const totalRow = this.db
383
+ .query(`SELECT COUNT(*) AS count FROM projection_runs ${where}`)
384
+ .get(...args) as { count: number }
385
+
386
+ let query = `
387
+ SELECT * FROM projection_runs
388
+ ${where}
389
+ ORDER BY started_at ${order}, id ${order}
390
+ `
391
+ const queryArgs = [...args]
392
+
393
+ if (limit !== undefined) {
394
+ assertProjectionRunListWindow(limit, "limit")
395
+ assertProjectionRunListWindow(offset, "offset")
396
+ query += " LIMIT ? OFFSET ?"
397
+ queryArgs.push(limit, offset)
398
+ } else if (offset > 0) {
399
+ assertProjectionRunListWindow(offset, "offset")
400
+ query += " LIMIT -1 OFFSET ?"
401
+ queryArgs.push(offset)
402
+ }
403
+
404
+ const rows = this.db.query(query).all(...queryArgs) as DatabaseRow[]
405
+ const runs = rows.map(rowToProjectionRunRecord)
406
+ return { runs, hasMore: offset + runs.length < totalRow.count, total: totalRow.count }
407
+ }
408
+
409
+ async listLatestByProjectionIds(
410
+ input: ListLatestProjectionRunsInput
411
+ ): Promise<ListLatestProjectionRunsResult> {
412
+ const rows = queryLatestRunsByOwnerId<DatabaseRow>({
413
+ db: this.db,
414
+ tableName: "projection_runs",
415
+ ownerColumn: "projection_id",
416
+ ownerIds: input.projectionIds,
417
+ projectId: input.projectId,
418
+ ownerIdFor: (row) => row.projection_id,
419
+ })
420
+ return { runs: rows.map(rowToProjectionRunRecord) }
421
+ }
422
+
423
+ close(): void {
424
+ closeSqliteStoreConnection(this.connection)
425
+ }
426
+
427
+ private updateProgress(
428
+ existing: DatabaseRow,
429
+ progress: ProjectionRunProgress,
430
+ executionToken: string
431
+ ): number {
432
+ const args: (string | number)[] = [
433
+ progress.sourceRowsRead,
434
+ progress.sourceRowsSkipped,
435
+ existing.project_id,
436
+ existing.id,
437
+ executionToken,
438
+ ]
439
+ const result = this.db
440
+ .query(
441
+ `
442
+ UPDATE projection_runs
443
+ SET
444
+ source_rows_read = ?,
445
+ source_rows_skipped = ?
446
+ WHERE project_id = ? AND id = ? AND status = 'running'
447
+ AND execution_token = ?
448
+ `
449
+ )
450
+ .run(...args)
451
+ return result.changes
452
+ }
453
+
454
+ private requireMaterializationExecution(
455
+ input: LockProjectionRunForMaterializationInput
456
+ ): DatabaseRow {
457
+ const row = this.requireRunning(input.projectId, input.id)
458
+ assertProjectionRunExecution(restoreProjectionRunRow(row), input)
459
+ return row
460
+ }
461
+
462
+ private requireRunning(projectId: string, id: string): DatabaseRow {
463
+ const row = this.requireRow(projectId, id)
464
+ assertProjectionRunRunning(restoreProjectionRunRow(row))
465
+ return row
466
+ }
467
+
468
+ private findRow(projectId: string, id: string): DatabaseRow | null {
469
+ return this.db
470
+ .query("SELECT * FROM projection_runs WHERE project_id = ? AND id = ?")
471
+ .get(projectId, id) as DatabaseRow | null
472
+ }
473
+
474
+ private requireRow(projectId: string, id: string): DatabaseRow {
475
+ assertProjectionRunNonEmpty(projectId, "projectId")
476
+ assertProjectionRunNonEmpty(id, "id")
477
+ const row = this.findRow(projectId, id)
478
+ if (!row) throw projectionRunNotFound(projectId, id)
479
+ return row
480
+ }
481
+ }
482
+
483
+ function createFreshExecutionToken(previous: string | null): string {
484
+ let token = randomUUID()
485
+ while (token === previous) token = randomUUID()
486
+ return token
487
+ }
488
+
489
+ function rowToProjectionRunRecord(row: DatabaseRow): ProjectionRunRecord {
490
+ return publicProjectionRunRecord(restoreProjectionRunRow(row))
491
+ }
492
+
493
+ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
494
+ const persisted: PersistedProjectionRunRecord = {
495
+ id: row.id,
496
+ projectId: row.project_id,
497
+ projectionId: row.projection_id,
498
+ projectionKind: row.projection_kind,
499
+ protocol: row.materialization_protocol ?? undefined,
500
+ datasetId: row.dataset_id,
501
+ datasetVersionId: row.dataset_version_id,
502
+ datasetVersionCreatedAt: row.dataset_version_created_at ?? undefined,
503
+ ontologyRevision: row.ontology_revision ?? undefined,
504
+ projectionRevision: row.projection_revision ?? undefined,
505
+ ownershipHash: row.ownership_hash ?? undefined,
506
+ objectTypeId: row.object_type_id ?? undefined,
507
+ sourceObjectTypeId: row.source_object_type_id ?? undefined,
508
+ targetObjectTypeId: row.target_object_type_id ?? undefined,
509
+ status: row.status,
510
+ startedAt: new Date(row.started_at),
511
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
512
+ attempt: databaseSafeInteger(row.attempt, "attempt"),
513
+ executionToken: row.execution_token ?? undefined,
514
+ fixedBatchSize: optionalDatabaseSafeInteger(row.fixed_batch_size, "fixedBatchSize"),
515
+ nextBatchOrdinal: optionalDatabaseSafeInteger(row.next_batch_ordinal, "nextBatchOrdinal"),
516
+ nextRowOffset: optionalDatabaseSafeInteger(row.next_row_offset, "nextRowOffset"),
517
+ inputExhausted: row.input_exhausted === null ? undefined : row.input_exhausted === 1,
518
+ missingTargetObjectTypeId: row.missing_target_object_type_id ?? undefined,
519
+ missingTargetObjectId: row.missing_target_object_id ?? undefined,
520
+ missingTargetBatchOrdinal: optionalDatabaseSafeInteger(
521
+ row.missing_target_batch_ordinal,
522
+ "missingTargetBatchOrdinal"
523
+ ),
524
+ missingTargetFirstSeenAt: row.missing_target_first_seen_at
525
+ ? new Date(row.missing_target_first_seen_at)
526
+ : undefined,
527
+ progress: {
528
+ sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
529
+ sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
530
+ },
531
+ errorMessage: row.error_message ?? undefined,
532
+ }
533
+ return restoreProjectionRun(persisted)
534
+ }
535
+
536
+ function projectionRunClaim(row: DatabaseRow): ProjectionRunClaim {
537
+ return createProjectionRunClaim(restoreProjectionRunRow(row))
538
+ }
539
+
540
+ function rowToTelemetryProjectionRunRecord(row: DatabaseRow): TelemetryProjectionRunRecord {
541
+ return requireTelemetryProjectionRun(rowToProjectionRunRecord(row))
542
+ }
543
+
544
+ function optionalDatabaseSafeInteger(value: number | null, fieldName: string): number | undefined {
545
+ return value === null ? undefined : databaseSafeInteger(value, fieldName)
546
+ }
547
+
548
+ function databaseSafeInteger(value: number, fieldName: string): number {
549
+ if (!Number.isSafeInteger(value) || value < 0) {
550
+ throw new ProjectionRunError(
551
+ `[SixbSqlite] Projection run persisted ${fieldName} is not a non-negative safe integer.`
552
+ )
553
+ }
554
+ return value
555
+ }
556
+
557
+ function isUniqueConstraintError(error: unknown): boolean {
558
+ return error instanceof Error && error.message.includes("UNIQUE constraint failed")
559
+ }
560
+
561
+ interface DatabaseRow {
562
+ project_id: string
563
+ id: string
564
+ projection_id: string
565
+ projection_kind: ProjectionKind
566
+ materialization_protocol: "replacement" | "telemetry" | null
567
+ dataset_id: string
568
+ dataset_version_id: string
569
+ dataset_version_created_at: string | null
570
+ ontology_revision: string | null
571
+ projection_revision: string | null
572
+ ownership_hash: string | null
573
+ object_type_id: string | null
574
+ source_object_type_id: string | null
575
+ target_object_type_id: string | null
576
+ status: ProjectionRunStatus
577
+ started_at: string
578
+ finished_at: string | null
579
+ attempt: number
580
+ execution_token: string | null
581
+ fixed_batch_size: number | null
582
+ next_batch_ordinal: number | null
583
+ next_row_offset: number | null
584
+ input_exhausted: 0 | 1 | null
585
+ missing_target_object_type_id: string | null
586
+ missing_target_object_id: string | null
587
+ missing_target_batch_ordinal: number | null
588
+ missing_target_first_seen_at: string | null
589
+ source_rows_read: number
590
+ source_rows_skipped: number
591
+ error_message: string | null
592
+ }