@sixb/pg 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,526 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import {
3
+ advanceProjectionTelemetry,
4
+ assertGenericProgressDoesNotAdvanceTelemetry,
5
+ assertProjectionMissingTarget,
6
+ assertProjectionRunExecution,
7
+ assertProjectionRunListWindow,
8
+ assertProjectionRunNonEmpty,
9
+ assertProjectionRunRunning,
10
+ assertProjectionRunStartInput,
11
+ createProjectionRunClaim,
12
+ immutableDatasetVersionConflict,
13
+ mergeProjectionRunProgress,
14
+ type PersistedProjectionRunRecord,
15
+ planProjectionRunFinish,
16
+ planProjectionRunReclaim,
17
+ projectionRunNotFound,
18
+ publicProjectionRunRecord,
19
+ requireProjectionRunExecutionToken,
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
+ ProjectionRunRecord,
36
+ ProjectionRunStatus,
37
+ ProjectionRunStorage,
38
+ RecordProjectionMissingTargetInput,
39
+ StartOrReclaimProjectionRunInput,
40
+ TelemetryProjectionRunRecord,
41
+ UpdateProjectionRunInput,
42
+ } from "@sixb/core/storage"
43
+ import { ProjectionRunError } from "@sixb/core/storage"
44
+ import { queryLatestRunsByOwnerId } from "./latest-run-query"
45
+ import type { SQLClient, SqlParameter } from "./pg-client"
46
+ import { lockAdvisoryKeys, type PgStoreClient, runPgTransaction } from "./transactions"
47
+
48
+ export class PgProjectionRunStorage implements ProjectionRunStorage {
49
+ constructor(private readonly sql: PgStoreClient) {}
50
+
51
+ async startOrReclaim(input: StartOrReclaimProjectionRunInput): Promise<ProjectionRunClaim> {
52
+ assertProjectionRunStartInput(input)
53
+
54
+ return runPgTransaction(this.sql, async (tx) => {
55
+ await lockAdvisoryKeys(tx, [
56
+ projectionRunLockKey(input.projectId, input.id),
57
+ datasetVersionLockKey(
58
+ input.projectId,
59
+ input.identity.datasetVersion.datasetId,
60
+ input.identity.datasetVersion.versionId
61
+ ),
62
+ ])
63
+ const [metadataConflict] = await tx<{ readonly id: string }[]>`
64
+ SELECT id FROM projection_runs
65
+ WHERE project_id = ${input.projectId}
66
+ AND id <> ${input.id}
67
+ AND dataset_id = ${input.identity.datasetVersion.datasetId}
68
+ AND dataset_version_id = ${input.identity.datasetVersion.versionId}
69
+ AND dataset_version_created_at IS NOT NULL
70
+ AND dataset_version_created_at <> ${input.identity.datasetVersion.createdAt}
71
+ LIMIT 1
72
+ `
73
+ if (metadataConflict) {
74
+ throw immutableDatasetVersionConflict(input.identity)
75
+ }
76
+
77
+ const [existingRow] = await tx<DatabaseRow[]>`
78
+ SELECT * FROM projection_runs
79
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
80
+ FOR UPDATE
81
+ `
82
+
83
+ const existing = existingRow ? restoreProjectionRunRow(existingRow) : undefined
84
+ const executionToken = createFreshExecutionToken(existing?.executionToken)
85
+
86
+ if (existingRow && existing) {
87
+ const { attempt } = planProjectionRunReclaim(existing, input)
88
+ const previousExecutionToken = requireProjectionRunExecutionToken(existing)
89
+
90
+ const [updated] = await tx<DatabaseRow[]>`
91
+ UPDATE projection_runs
92
+ SET attempt = ${attempt}, execution_token = ${executionToken}
93
+ WHERE project_id = ${input.projectId}
94
+ AND id = ${input.id}
95
+ AND status = ${"running"}
96
+ AND execution_token = ${previousExecutionToken}
97
+ RETURNING *
98
+ `
99
+ if (!updated) throw staleProjectionRunExecution(input.id)
100
+ return projectionRunClaim(updated)
101
+ }
102
+
103
+ const [inserted] = await tx<DatabaseRow[]>`
104
+ INSERT INTO projection_runs (
105
+ project_id,
106
+ id,
107
+ projection_id,
108
+ projection_kind,
109
+ dataset_id,
110
+ dataset_version_id,
111
+ object_type_id,
112
+ source_object_type_id,
113
+ target_object_type_id,
114
+ status,
115
+ started_at,
116
+ attempt,
117
+ execution_token,
118
+ materialization_protocol,
119
+ dataset_version_created_at,
120
+ ontology_revision,
121
+ projection_revision,
122
+ ownership_hash,
123
+ fixed_batch_size,
124
+ next_batch_ordinal,
125
+ next_row_offset,
126
+ input_exhausted
127
+ ) VALUES (
128
+ ${input.projectId},
129
+ ${input.id},
130
+ ${input.identity.projectionId},
131
+ ${input.identity.projectionKind},
132
+ ${input.identity.datasetVersion.datasetId},
133
+ ${input.identity.datasetVersion.versionId},
134
+ ${"objectTypeId" in input.target ? input.target.objectTypeId : null},
135
+ ${"sourceObjectTypeId" in input.target ? input.target.sourceObjectTypeId : null},
136
+ ${"sourceObjectTypeId" in input.target ? input.target.targetObjectTypeId : null},
137
+ ${"running"},
138
+ ${input.startedAt ?? new Date()},
139
+ ${1},
140
+ ${executionToken},
141
+ ${input.identity.protocol},
142
+ ${input.identity.datasetVersion.createdAt},
143
+ ${input.identity.ontologyRevision},
144
+ ${input.identity.projectionRevision},
145
+ ${input.identity.ownershipHash},
146
+ ${input.fixedBatchSize ?? null},
147
+ ${input.fixedBatchSize === undefined ? null : 0},
148
+ ${input.fixedBatchSize === undefined ? null : 0},
149
+ ${input.fixedBatchSize === undefined ? null : false}
150
+ )
151
+ RETURNING *
152
+ `
153
+
154
+ return projectionRunClaim(inserted)
155
+ })
156
+ }
157
+
158
+ async lockForMaterialization(
159
+ input: LockProjectionRunForMaterializationInput
160
+ ): Promise<ProjectionRunRecord> {
161
+ return runPgTransaction(this.sql, async (tx) => {
162
+ const row = await requireMaterializationExecution(tx, input)
163
+ return rowToProjectionRunRecord(row)
164
+ })
165
+ }
166
+
167
+ async update(input: UpdateProjectionRunInput): Promise<ProjectionRunRecord> {
168
+ return runPgTransaction(this.sql, async (tx) => {
169
+ const existingRow = await requireMaterializationExecution(tx, input)
170
+ const existing = restoreProjectionRunRow(existingRow)
171
+ assertGenericProgressDoesNotAdvanceTelemetry(existing, input.progress)
172
+ const progress = mergeProjectionRunProgress(existing.progress, input.progress)
173
+ const [updated] = await tx<DatabaseRow[]>`
174
+ UPDATE projection_runs
175
+ SET
176
+ source_rows_read = ${progress.sourceRowsRead},
177
+ source_rows_skipped = ${progress.sourceRowsSkipped}
178
+ WHERE project_id = ${input.projectId}
179
+ AND id = ${input.id}
180
+ AND status = ${"running"}
181
+ AND execution_token = ${input.executionToken}
182
+ RETURNING *
183
+ `
184
+ if (!updated) throw staleProjectionRunExecution(input.id)
185
+ return rowToProjectionRunRecord(updated)
186
+ })
187
+ }
188
+
189
+ async finish(input: FinishProjectionRunInput): Promise<ProjectionRunRecord> {
190
+ return runPgTransaction(this.sql, async (tx) => {
191
+ const existingRow = await requireMaterializationExecution(tx, input)
192
+ const existing = restoreProjectionRunRow(existingRow)
193
+ const plan = planProjectionRunFinish(existing, input)
194
+ const [updated] = await tx<DatabaseRow[]>`
195
+ UPDATE projection_runs
196
+ SET
197
+ status = ${input.status},
198
+ finished_at = ${plan.finishedAt},
199
+ execution_token = ${null},
200
+ source_rows_read = ${plan.progress.sourceRowsRead},
201
+ source_rows_skipped = ${plan.progress.sourceRowsSkipped},
202
+ input_exhausted = ${plan.inputExhausted ?? existingRow.input_exhausted},
203
+ error_message = ${plan.errorMessage ?? null}
204
+ WHERE project_id = ${input.projectId}
205
+ AND id = ${input.id}
206
+ AND status = ${"running"}
207
+ AND execution_token = ${input.executionToken}
208
+ RETURNING *
209
+ `
210
+ if (!updated) throw staleProjectionRunExecution(input.id)
211
+ return rowToProjectionRunRecord(updated)
212
+ })
213
+ }
214
+
215
+ async advanceTelemetryCheckpoint(
216
+ input: AdvanceProjectionTelemetryCheckpointInput
217
+ ): Promise<TelemetryProjectionRunRecord> {
218
+ return runPgTransaction(this.sql, async (tx) => {
219
+ const existingRow = await requireMaterializationExecution(tx, input)
220
+ const advance = advanceProjectionTelemetry(restoreProjectionRunRow(existingRow), input)
221
+ const [updated] = await tx<DatabaseRow[]>`
222
+ UPDATE projection_runs
223
+ SET
224
+ next_batch_ordinal = ${advance.checkpoint.nextBatchOrdinal},
225
+ next_row_offset = ${advance.checkpoint.nextRowOffset},
226
+ input_exhausted = ${advance.checkpoint.inputExhausted},
227
+ source_rows_read = ${advance.progress.sourceRowsRead},
228
+ source_rows_skipped = ${advance.progress.sourceRowsSkipped},
229
+ missing_target_object_type_id = ${null},
230
+ missing_target_object_id = ${null},
231
+ missing_target_batch_ordinal = ${null},
232
+ missing_target_first_seen_at = ${null}
233
+ WHERE project_id = ${input.projectId}
234
+ AND id = ${input.id}
235
+ AND status = ${"running"}
236
+ AND execution_token = ${input.executionToken}
237
+ AND next_batch_ordinal = ${input.batchOrdinal}
238
+ AND input_exhausted = ${false}
239
+ RETURNING *
240
+ `
241
+ if (!updated) throw staleProjectionRunExecution(input.id)
242
+ return rowToTelemetryProjectionRunRecord(updated)
243
+ })
244
+ }
245
+
246
+ async recordMissingTarget(
247
+ input: RecordProjectionMissingTargetInput
248
+ ): Promise<TelemetryProjectionRunRecord> {
249
+ return runPgTransaction(this.sql, async (tx) => {
250
+ const existingRow = await requireMaterializationExecution(tx, input)
251
+ const existing = requireTelemetryProjectionRun(restoreProjectionRunRow(existingRow))
252
+ const missingTarget = assertProjectionMissingTarget(existing, input.missingTarget)
253
+ const [updated] = await tx<DatabaseRow[]>`
254
+ UPDATE projection_runs
255
+ SET
256
+ missing_target_object_type_id = ${missingTarget.objectTypeId},
257
+ missing_target_object_id = ${missingTarget.objectId},
258
+ missing_target_batch_ordinal = ${missingTarget.batchOrdinal},
259
+ missing_target_first_seen_at = ${missingTarget.firstSeenAt}
260
+ WHERE project_id = ${input.projectId}
261
+ AND id = ${input.id}
262
+ AND status = ${"running"}
263
+ AND execution_token = ${input.executionToken}
264
+ AND next_batch_ordinal = ${missingTarget.batchOrdinal}
265
+ RETURNING *
266
+ `
267
+ if (!updated) throw staleProjectionRunExecution(input.id)
268
+ return rowToTelemetryProjectionRunRecord(updated)
269
+ })
270
+ }
271
+
272
+ async getById(params: { projectId: string; id: string }): Promise<ProjectionRunRecord | null> {
273
+ const [row] = await this.sql<DatabaseRow[]>`
274
+ SELECT * FROM projection_runs
275
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
276
+ `
277
+
278
+ return row ? rowToProjectionRunRecord(row) : null
279
+ }
280
+
281
+ async list(input: ListProjectionRunsInput): Promise<ListProjectionRunsResult> {
282
+ assertProjectionRunNonEmpty(input.projectId, "projectId")
283
+
284
+ if (input.statuses && input.statuses.length === 0) {
285
+ return { runs: [], hasMore: false, total: 0 }
286
+ }
287
+
288
+ const whereClauses = ["project_id = $1"]
289
+ const params: SqlParameter[] = [input.projectId]
290
+ let index = 2
291
+
292
+ if (input.projectionId) {
293
+ whereClauses.push(`projection_id = $${index++}`)
294
+ params.push(input.projectionId)
295
+ }
296
+ if (input.projectionKind) {
297
+ whereClauses.push(`projection_kind = $${index++}`)
298
+ params.push(input.projectionKind)
299
+ }
300
+ if (input.datasetId) {
301
+ whereClauses.push(`dataset_id = $${index++}`)
302
+ params.push(input.datasetId)
303
+ }
304
+ if (input.datasetVersionId) {
305
+ whereClauses.push(`dataset_version_id = $${index++}`)
306
+ params.push(input.datasetVersionId)
307
+ }
308
+
309
+ if (input.objectTypeIds) {
310
+ if (input.objectTypeIds.length === 0) {
311
+ return { runs: [], hasMore: false, total: 0 }
312
+ }
313
+ const placeholders = input.objectTypeIds.map(() => `$${index++}`)
314
+ const list = placeholders.join(", ")
315
+ const sourceList = input.objectTypeIds.map(() => `$${index++}`).join(", ")
316
+ const targetList = input.objectTypeIds.map(() => `$${index++}`).join(", ")
317
+ whereClauses.push(
318
+ `(object_type_id IN (${list})` +
319
+ ` OR (source_object_type_id IN (${sourceList})` +
320
+ ` AND target_object_type_id IN (${targetList})))`
321
+ )
322
+ params.push(...input.objectTypeIds, ...input.objectTypeIds, ...input.objectTypeIds)
323
+ }
324
+
325
+ if (input.statuses) {
326
+ const placeholders = input.statuses.map(() => `$${index++}`)
327
+ whereClauses.push(`status IN (${placeholders.join(", ")})`)
328
+ params.push(...input.statuses)
329
+ }
330
+ if (input.startedAfter) {
331
+ whereClauses.push(`started_at >= $${index++}`)
332
+ params.push(input.startedAfter)
333
+ }
334
+ if (input.startedBefore) {
335
+ whereClauses.push(`started_at <= $${index++}`)
336
+ params.push(input.startedBefore)
337
+ }
338
+
339
+ const where = `WHERE ${whereClauses.join(" AND ")}`
340
+ const order = input.order === "asc" ? "ASC" : "DESC"
341
+ const offset = input.offset ?? 0
342
+ assertProjectionRunListWindow(input.limit, "limit")
343
+ assertProjectionRunListWindow(offset, "offset")
344
+
345
+ const [totalRow] = await this.sql.unsafe<{ count: string | number }[]>(
346
+ `SELECT COUNT(*)::bigint AS count FROM projection_runs ${where}`,
347
+ params
348
+ )
349
+
350
+ const queryParams = [...params]
351
+ let query = `
352
+ SELECT * FROM projection_runs
353
+ ${where}
354
+ ORDER BY started_at ${order}, id ${order}
355
+ `
356
+ if (input.limit !== undefined) {
357
+ query += ` LIMIT $${index++} OFFSET $${index++}`
358
+ queryParams.push(input.limit, offset)
359
+ } else if (offset > 0) {
360
+ query += ` OFFSET $${index++}`
361
+ queryParams.push(offset)
362
+ }
363
+
364
+ const rows = await this.sql.unsafe<DatabaseRow[]>(query, queryParams)
365
+ const total = Number(totalRow?.count ?? 0)
366
+ const runs = rows.map(rowToProjectionRunRecord)
367
+ return { runs, hasMore: offset + runs.length < total, total }
368
+ }
369
+
370
+ async listLatestByProjectionIds(
371
+ input: ListLatestProjectionRunsInput
372
+ ): Promise<ListLatestProjectionRunsResult> {
373
+ const rows = await queryLatestRunsByOwnerId<DatabaseRow>({
374
+ sql: this.sql,
375
+ tableName: "projection_runs",
376
+ ownerColumn: "projection_id",
377
+ ownerIds: input.projectionIds,
378
+ projectId: input.projectId,
379
+ ownerIdFor: (row) => row.projection_id,
380
+ })
381
+
382
+ return { runs: rows.map(rowToProjectionRunRecord) }
383
+ }
384
+ }
385
+
386
+ async function requireMaterializationExecution(
387
+ sql: SQLClient,
388
+ input: LockProjectionRunForMaterializationInput
389
+ ): Promise<DatabaseRow> {
390
+ const row = await requireRunning(sql, input.projectId, input.id)
391
+ assertProjectionRunExecution(restoreProjectionRunRow(row), input)
392
+ return row
393
+ }
394
+
395
+ async function requireRunning(sql: SQLClient, projectId: string, id: string): Promise<DatabaseRow> {
396
+ assertProjectionRunNonEmpty(projectId, "projectId")
397
+ assertProjectionRunNonEmpty(id, "id")
398
+
399
+ const [row] = await sql<DatabaseRow[]>`
400
+ SELECT * FROM projection_runs
401
+ WHERE project_id = ${projectId} AND id = ${id}
402
+ FOR UPDATE
403
+ `
404
+ if (!row) throw projectionRunNotFound(projectId, id)
405
+ assertProjectionRunRunning(restoreProjectionRunRow(row))
406
+ return row
407
+ }
408
+
409
+ function createFreshExecutionToken(previous: string | undefined): string {
410
+ let token = randomUUID()
411
+ while (token === previous) token = randomUUID()
412
+ return token
413
+ }
414
+
415
+ function projectionRunLockKey(projectId: string, id: string): string {
416
+ return `projection-run:${JSON.stringify([projectId, id])}`
417
+ }
418
+
419
+ function datasetVersionLockKey(projectId: string, datasetId: string, versionId: string): string {
420
+ return `projection-dataset-version:${JSON.stringify([projectId, datasetId, versionId])}`
421
+ }
422
+
423
+ function rowToProjectionRunRecord(row: DatabaseRow): ProjectionRunRecord {
424
+ return publicProjectionRunRecord(restoreProjectionRunRow(row))
425
+ }
426
+
427
+ function projectionRunClaim(row: DatabaseRow): ProjectionRunClaim {
428
+ return createProjectionRunClaim(restoreProjectionRunRow(row))
429
+ }
430
+
431
+ function rowToTelemetryProjectionRunRecord(row: DatabaseRow): TelemetryProjectionRunRecord {
432
+ return requireTelemetryProjectionRun(rowToProjectionRunRecord(row))
433
+ }
434
+
435
+ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
436
+ const persisted: PersistedProjectionRunRecord = {
437
+ id: row.id,
438
+ projectId: row.project_id,
439
+ projectionId: row.projection_id,
440
+ projectionKind: row.projection_kind,
441
+ protocol: row.materialization_protocol ?? undefined,
442
+ datasetId: row.dataset_id,
443
+ datasetVersionId: row.dataset_version_id,
444
+ datasetVersionCreatedAt: row.dataset_version_created_at ?? undefined,
445
+ ontologyRevision: row.ontology_revision ?? undefined,
446
+ projectionRevision: row.projection_revision ?? undefined,
447
+ ownershipHash: row.ownership_hash ?? undefined,
448
+ objectTypeId: row.object_type_id ?? undefined,
449
+ sourceObjectTypeId: row.source_object_type_id ?? undefined,
450
+ targetObjectTypeId: row.target_object_type_id ?? undefined,
451
+ status: row.status,
452
+ startedAt: new Date(row.started_at),
453
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
454
+ attempt: databaseSafeInteger(row.attempt, "attempt"),
455
+ executionToken: row.execution_token ?? undefined,
456
+ fixedBatchSize: optionalDatabaseSafeInteger(row.fixed_batch_size, "fixedBatchSize"),
457
+ nextBatchOrdinal: optionalDatabaseSafeInteger(row.next_batch_ordinal, "nextBatchOrdinal"),
458
+ nextRowOffset: optionalDatabaseSafeInteger(row.next_row_offset, "nextRowOffset"),
459
+ inputExhausted: row.input_exhausted ?? undefined,
460
+ missingTargetObjectTypeId: row.missing_target_object_type_id ?? undefined,
461
+ missingTargetObjectId: row.missing_target_object_id ?? undefined,
462
+ missingTargetBatchOrdinal: optionalDatabaseSafeInteger(
463
+ row.missing_target_batch_ordinal,
464
+ "missingTargetBatchOrdinal"
465
+ ),
466
+ missingTargetFirstSeenAt: row.missing_target_first_seen_at
467
+ ? new Date(row.missing_target_first_seen_at)
468
+ : undefined,
469
+ progress: {
470
+ sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
471
+ sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
472
+ },
473
+ errorMessage: row.error_message ?? undefined,
474
+ }
475
+ return restoreProjectionRun(persisted)
476
+ }
477
+
478
+ function optionalDatabaseSafeInteger(
479
+ value: number | string | null,
480
+ fieldName: string
481
+ ): number | undefined {
482
+ return value === null ? undefined : databaseSafeInteger(value, fieldName)
483
+ }
484
+
485
+ function databaseSafeInteger(value: number | string | null, fieldName: string): number {
486
+ const result = Number(value)
487
+ if (!Number.isSafeInteger(result) || result < 0) {
488
+ throw new ProjectionRunError(
489
+ `[SixbPg] Projection run persisted ${fieldName} is not a non-negative safe integer.`
490
+ )
491
+ }
492
+ return result
493
+ }
494
+
495
+ interface DatabaseRow {
496
+ project_id: string
497
+ id: string
498
+ projection_id: string
499
+ projection_kind: ProjectionKind
500
+ dataset_id: string
501
+ dataset_version_id: string
502
+ object_type_id: string | null
503
+ source_object_type_id: string | null
504
+ target_object_type_id: string | null
505
+ status: ProjectionRunStatus
506
+ started_at: Date | string
507
+ finished_at: Date | string | null
508
+ attempt: number | string
509
+ execution_token: string | null
510
+ materialization_protocol: "replacement" | "telemetry" | null
511
+ dataset_version_created_at: string | null
512
+ ontology_revision: string | null
513
+ projection_revision: string | null
514
+ ownership_hash: string | null
515
+ fixed_batch_size: number | string | null
516
+ next_batch_ordinal: number | string | null
517
+ next_row_offset: number | string | null
518
+ input_exhausted: boolean | null
519
+ missing_target_object_type_id: string | null
520
+ missing_target_object_id: string | null
521
+ missing_target_batch_ordinal: number | string | null
522
+ missing_target_first_seen_at: Date | string | null
523
+ source_rows_read: number | string
524
+ source_rows_skipped: number | string
525
+ error_message: string | null
526
+ }