@sixb/sqlite 0.1.0 → 0.1.2

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 (110) hide show
  1. package/README.md +18 -13
  2. package/dist/action-run-storage.d.ts +5 -2
  3. package/dist/action-run-storage.d.ts.map +1 -1
  4. package/dist/agents/agent-storage.d.ts +4 -2
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/rows.d.ts +3 -10
  7. package/dist/agents/rows.d.ts.map +1 -1
  8. package/dist/agents/runs.d.ts +3 -2
  9. package/dist/agents/runs.d.ts.map +1 -1
  10. package/dist/ai-usage-storage.d.ts +22 -0
  11. package/dist/ai-usage-storage.d.ts.map +1 -0
  12. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  13. package/dist/auth-storage/shared.d.ts +1 -10
  14. package/dist/auth-storage/shared.d.ts.map +1 -1
  15. package/dist/connector-connection-storage.d.ts +9 -0
  16. package/dist/connector-connection-storage.d.ts.map +1 -0
  17. package/dist/execution-storage.d.ts +14 -0
  18. package/dist/execution-storage.d.ts.map +1 -0
  19. package/dist/index.d.ts +13 -9
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4114 -1149
  22. package/dist/index.js.map +30 -27
  23. package/dist/latest-run-query.d.ts.map +1 -1
  24. package/dist/migrations.d.ts +0 -1
  25. package/dist/migrations.d.ts.map +1 -1
  26. package/dist/ontology-storage/materialization-session.d.ts +50 -8
  27. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  28. package/dist/ontology-storage/materialization-state.d.ts +4 -5
  29. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +0 -2
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/shared.d.ts +3 -3
  35. package/dist/ontology-storage/shared.d.ts.map +1 -1
  36. package/dist/ontology-storage/sources.d.ts.map +1 -1
  37. package/dist/pipeline-run-storage.d.ts +7 -2
  38. package/dist/pipeline-run-storage.d.ts.map +1 -1
  39. package/dist/projection-run-storage.d.ts +9 -2
  40. package/dist/projection-run-storage.d.ts.map +1 -1
  41. package/dist/run-list-query.d.ts +1 -1
  42. package/dist/run-list-query.d.ts.map +1 -1
  43. package/dist/sync-run-storage.d.ts +7 -2
  44. package/dist/sync-run-storage.d.ts.map +1 -1
  45. package/dist/testing.d.ts +6 -0
  46. package/dist/testing.d.ts.map +1 -0
  47. package/dist/transactions.d.ts +3 -0
  48. package/dist/transactions.d.ts.map +1 -1
  49. package/dist/webhook-run-storage.d.ts +9 -2
  50. package/dist/webhook-run-storage.d.ts.map +1 -1
  51. package/dist/workflow-run-storage.d.ts +7 -3
  52. package/dist/workflow-run-storage.d.ts.map +1 -1
  53. package/package.json +4 -3
  54. package/src/action-run-storage.ts +51 -74
  55. package/src/agents/agent-storage.ts +5 -3
  56. package/src/agents/rows.ts +8 -40
  57. package/src/agents/runs.ts +27 -21
  58. package/src/ai-usage-storage.ts +324 -0
  59. package/src/auth-storage/service-accounts.ts +14 -21
  60. package/src/auth-storage/shared.ts +0 -22
  61. package/src/connector-connection-storage.ts +784 -0
  62. package/src/execution-storage.ts +158 -0
  63. package/src/index.ts +63 -22
  64. package/src/latest-run-query.ts +7 -1
  65. package/src/migrations/002-workflow-run-output.sql +27 -0
  66. package/src/migrations/003-merge-sync-runs.sql +71 -0
  67. package/src/migrations/004-executions.sql +142 -0
  68. package/src/migrations/005-workflow-executions.sql +7 -0
  69. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  70. package/src/migrations/007-split-overrides.sql +154 -0
  71. package/src/migrations/008-action-executions.sql +4 -0
  72. package/src/migrations/009-agent-executions.sql +115 -0
  73. package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
  74. package/src/migrations/011-sync-failure-record.sql +22 -0
  75. package/src/migrations/012-pipeline-failure-record.sql +45 -0
  76. package/src/migrations/013-workflow-failure-record.sql +45 -0
  77. package/src/migrations/014-agent-failure-record.sql +59 -0
  78. package/src/migrations/015-projection-failure-record.sql +20 -0
  79. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  80. package/src/migrations/017-action-failure-record.sql +65 -0
  81. package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
  82. package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
  83. package/src/migrations/020-drop-run-usage-projections.sql +7 -0
  84. package/src/migrations/021-sync-pipeline-executions.sql +101 -0
  85. package/src/migrations/022-projection-executions.sql +244 -0
  86. package/src/migrations/023-webhook-executions.sql +51 -0
  87. package/src/migrations/024-ontology-commit-executions.sql +58 -0
  88. package/src/migrations/025-connector-connections.sql +257 -0
  89. package/src/migrations.ts +126 -5
  90. package/src/ontology-storage/materialization-session.ts +317 -143
  91. package/src/ontology-storage/materialization-state.ts +398 -150
  92. package/src/ontology-storage/materialization-writer.ts +142 -67
  93. package/src/ontology-storage/materializations.ts +67 -141
  94. package/src/ontology-storage/outbox.ts +10 -3
  95. package/src/ontology-storage/shared.ts +10 -4
  96. package/src/ontology-storage/sources.ts +4 -1
  97. package/src/pipeline-run-storage.ts +128 -43
  98. package/src/projection-run-storage.ts +209 -97
  99. package/src/run-list-query.ts +9 -4
  100. package/src/sync-run-storage.ts +120 -34
  101. package/src/testing.ts +20 -0
  102. package/src/transactions.ts +10 -3
  103. package/src/webhook-run-storage.ts +118 -74
  104. package/src/workflow-run-storage.ts +124 -133
  105. package/dist/validation.d.ts +0 -5
  106. package/dist/validation.d.ts.map +0 -1
  107. package/dist/webhook-delivery-storage.d.ts +0 -27
  108. package/dist/webhook-delivery-storage.d.ts.map +0 -1
  109. package/src/validation.ts +0 -30
  110. package/src/webhook-delivery-storage.ts +0 -272
@@ -1,15 +1,21 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import { randomUUID } from "node:crypto"
3
+ import { serializeSixbFailure } from "@sixb/core/internal/errors"
3
4
  import {
4
5
  advanceProjectionTelemetry,
5
6
  assertGenericProgressDoesNotAdvanceTelemetry,
6
7
  assertProjectionMissingTarget,
7
- assertProjectionRunExecution,
8
+ assertProjectionRunAttempt,
9
+ assertProjectionRunDurableExecution,
8
10
  assertProjectionRunListWindow,
9
11
  assertProjectionRunNonEmpty,
12
+ assertProjectionRunQueueInput,
10
13
  assertProjectionRunRunning,
11
14
  assertProjectionRunStartInput,
15
+ canRequeueProjectionRunAfterEnqueueFailure,
12
16
  createProjectionRunClaim,
17
+ createProjectionRunRecord,
18
+ failProjectionRunEnqueue,
13
19
  immutableDatasetVersionConflict,
14
20
  mergeProjectionRunProgress,
15
21
  type PersistedProjectionRunRecord,
@@ -24,6 +30,8 @@ import {
24
30
  } from "@sixb/core/internal/projection-run-storage-provider"
25
31
  import type {
26
32
  AdvanceProjectionTelemetryCheckpointInput,
33
+ ExecutionStorage,
34
+ FailProjectionRunEnqueueInput,
27
35
  FinishProjectionRunInput,
28
36
  ListLatestProjectionRunsInput,
29
37
  ListLatestProjectionRunsResult,
@@ -36,12 +44,13 @@ import type {
36
44
  ProjectionRunRecord,
37
45
  ProjectionRunStatus,
38
46
  ProjectionRunStorage,
47
+ QueueProjectionRunInput,
39
48
  RecordProjectionMissingTargetInput,
40
49
  StartOrReclaimProjectionRunInput,
41
50
  TelemetryProjectionRunRecord,
42
51
  UpdateProjectionRunInput,
43
52
  } from "@sixb/core/storage"
44
- import { ProjectionRunError } from "@sixb/core/storage"
53
+ import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage"
45
54
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
46
55
  import { installFreshSqliteSchema } from "./migrations"
47
56
  import {
@@ -52,6 +61,8 @@ import {
52
61
  } from "./transactions"
53
62
 
54
63
  export interface SqliteProjectionRunStorageOptions {
64
+ /** Execution lookup sharing the same provider transaction. */
65
+ executions: ExecutionStorage
55
66
  /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
56
67
  path?: string
57
68
  /** Internal shared connection used by bundled SqliteStorage. */
@@ -61,16 +72,76 @@ export interface SqliteProjectionRunStorageOptions {
61
72
  export class SqliteProjectionRunStorage implements ProjectionRunStorage {
62
73
  private readonly connection: SqliteStoreConnection
63
74
  private readonly db: Database
75
+ private readonly executions: ExecutionStorage
64
76
 
65
- constructor(options: SqliteProjectionRunStorageOptions = {}) {
77
+ constructor(options: SqliteProjectionRunStorageOptions) {
66
78
  this.connection = openSqliteStoreConnection(options)
67
79
  this.db = this.connection.db
80
+ this.executions = options.executions
68
81
 
69
82
  if (this.connection.installFreshSchema) {
70
83
  installFreshSqliteSchema(this.db)
71
84
  }
72
85
  }
73
86
 
87
+ async queue(input: QueueProjectionRunInput): Promise<ProjectionRunRecord> {
88
+ assertProjectionRunQueueInput(input)
89
+ await assertProjectionRunDurableExecution({
90
+ executions: this.executions,
91
+ projectId: input.projectId,
92
+ executionId: input.executionId,
93
+ runId: input.id,
94
+ projectionId: input.identity.projectionId,
95
+ datasetId: input.identity.datasetVersion.datasetId,
96
+ datasetVersionId: input.identity.datasetVersion.versionId,
97
+ })
98
+
99
+ return runImmediateTransaction(this.db, () => {
100
+ this.assertDatasetVersionIsImmutable(input)
101
+ const existing = this.findRow(input.projectId, input.id)
102
+ if (existing) {
103
+ const restored = restoreProjectionRunRow(existing)
104
+ if (!canRequeueProjectionRunAfterEnqueueFailure(restored, input)) {
105
+ throw duplicateProjectionRun(input)
106
+ }
107
+ const queuedAt = input.queuedAt ?? new Date()
108
+ const result = this.db
109
+ .query(
110
+ `
111
+ UPDATE projection_runs
112
+ SET status = 'queued', queued_at = ?, started_at = NULL, finished_at = NULL,
113
+ attempt = 0, execution_token = NULL, next_batch_ordinal = ?, next_row_offset = ?,
114
+ input_exhausted = ?, missing_target_object_type_id = NULL,
115
+ missing_target_object_id = NULL, missing_target_batch_ordinal = NULL,
116
+ missing_target_first_seen_at = NULL, source_rows_read = 0,
117
+ source_rows_skipped = 0, error = NULL
118
+ WHERE project_id = ? AND id = ? AND status = 'failed'
119
+ AND json_extract(error, '$.code') = 'queue.enqueue_failed'
120
+ `
121
+ )
122
+ .run(
123
+ queuedAt.toISOString(),
124
+ input.fixedBatchSize === undefined ? null : 0,
125
+ input.fixedBatchSize === undefined ? null : 0,
126
+ input.fixedBatchSize === undefined ? null : 0,
127
+ input.projectId,
128
+ input.id
129
+ )
130
+ if (result.changes !== 1) throw duplicateProjectionRun(input)
131
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id))
132
+ }
133
+
134
+ const record = createProjectionRunRecord(input)
135
+ try {
136
+ this.insertQueued(record)
137
+ } catch (error) {
138
+ if (isUniqueConstraintError(error)) throw duplicateProjectionRun(input)
139
+ throw error
140
+ }
141
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id))
142
+ })
143
+ }
144
+
74
145
  async startOrReclaim(input: StartOrReclaimProjectionRunInput): Promise<ProjectionRunClaim> {
75
146
  return runImmediateTransaction(this.db, () => {
76
147
  assertProjectionRunStartInput(input)
@@ -96,98 +167,64 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
96
167
  throw immutableDatasetVersionConflict(input.identity)
97
168
  }
98
169
 
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)
170
+ const existing = this.requireRow(input.projectId, input.id)
171
+ const existingRecord = restoreProjectionRunRow(existing)
172
+ const attempt = planProjectionRunReclaim(existingRecord, input).attempt
107
173
 
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 = ?
174
+ const executionToken = createFreshExecutionToken(existing.execution_token)
175
+ const result = this.db
176
+ .query(
115
177
  `
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
178
+ UPDATE projection_runs
179
+ SET status = 'running', started_at = COALESCE(started_at, ?), finished_at = NULL,
180
+ attempt = ?, execution_token = ?, error = NULL
181
+ WHERE project_id = ? AND id = ?
182
+ AND (
183
+ (status = 'queued' AND execution_token IS NULL)
184
+ OR (status = 'running' AND execution_token = ?)
152
185
  )
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
+ )
188
+ .run(
189
+ (input.startedAt ?? new Date()).toISOString(),
190
+ attempt,
191
+ executionToken,
192
+ input.projectId,
193
+ input.id,
194
+ existing.execution_token
195
+ )
196
+ if (result.changes !== 1) throw staleProjectionRunExecution(input.id)
186
197
 
187
198
  return projectionRunClaim(this.requireRow(input.projectId, input.id))
188
199
  })
189
200
  }
190
201
 
202
+ async failEnqueue(input: FailProjectionRunEnqueueInput): Promise<ProjectionRunRecord> {
203
+ return runImmediateTransaction(this.db, () => {
204
+ const existing = restoreProjectionRunRow(this.requireRow(input.projectId, input.id))
205
+ const failed = failProjectionRunEnqueue(existing, input)
206
+ if (!failed.finishedAt || !failed.error) {
207
+ throw new ProjectionRunError(`[SixbSqlite] Projection run '${input.id}' did not finish.`)
208
+ }
209
+ const result = this.db
210
+ .query(
211
+ `
212
+ UPDATE projection_runs
213
+ SET status = 'failed', finished_at = ?, error = ?
214
+ WHERE project_id = ? AND id = ? AND status = 'queued'
215
+ `
216
+ )
217
+ .run(
218
+ failed.finishedAt.toISOString(),
219
+ serializeSixbFailure(failed.error, PROJECTION_RUN_FAILURE_CODES),
220
+ input.projectId,
221
+ input.id
222
+ )
223
+ if (result.changes !== 1) throw duplicateProjectionRun(input)
224
+ return rowToProjectionRunRecord(this.requireRow(input.projectId, input.id))
225
+ })
226
+ }
227
+
191
228
  async lockForMaterialization(
192
229
  input: LockProjectionRunForMaterializationInput
193
230
  ): Promise<ProjectionRunRecord> {
@@ -221,7 +258,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
221
258
  source_rows_read = ?,
222
259
  source_rows_skipped = ?,
223
260
  input_exhausted = ?,
224
- error_message = ?
261
+ error = ?
225
262
  WHERE project_id = ? AND id = ? AND status = 'running' AND execution_token = ?
226
263
  `
227
264
  )
@@ -231,7 +268,9 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
231
268
  plan.progress.sourceRowsRead,
232
269
  plan.progress.sourceRowsSkipped,
233
270
  plan.inputExhausted ? 1 : existingRow.input_exhausted,
234
- plan.errorMessage ?? null,
271
+ plan.error === undefined
272
+ ? null
273
+ : serializeSixbFailure(plan.error, PROJECTION_RUN_FAILURE_CODES),
235
274
  input.projectId,
236
275
  input.id,
237
276
  input.executionToken
@@ -366,11 +405,11 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
366
405
  args.push(...input.statuses)
367
406
  }
368
407
  if (input.startedAfter) {
369
- whereClauses.push("started_at >= ?")
408
+ whereClauses.push("COALESCE(started_at, queued_at) >= ?")
370
409
  args.push(input.startedAfter.toISOString())
371
410
  }
372
411
  if (input.startedBefore) {
373
- whereClauses.push("started_at <= ?")
412
+ whereClauses.push("COALESCE(started_at, queued_at) <= ?")
374
413
  args.push(input.startedBefore.toISOString())
375
414
  }
376
415
 
@@ -386,7 +425,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
386
425
  let query = `
387
426
  SELECT * FROM projection_runs
388
427
  ${where}
389
- ORDER BY started_at ${order}, id ${order}
428
+ ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
390
429
  `
391
430
  const queryArgs = [...args]
392
431
 
@@ -424,6 +463,69 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
424
463
  closeSqliteStoreConnection(this.connection)
425
464
  }
426
465
 
466
+ private assertDatasetVersionIsImmutable(input: QueueProjectionRunInput): void {
467
+ const conflict = this.db
468
+ .query(
469
+ `
470
+ SELECT 1 FROM projection_runs
471
+ WHERE project_id = ? AND id <> ? AND dataset_id = ? AND dataset_version_id = ?
472
+ AND dataset_version_created_at <> ?
473
+ LIMIT 1
474
+ `
475
+ )
476
+ .get(
477
+ input.projectId,
478
+ input.id,
479
+ input.identity.datasetVersion.datasetId,
480
+ input.identity.datasetVersion.versionId,
481
+ input.identity.datasetVersion.createdAt
482
+ )
483
+ if (conflict) throw immutableDatasetVersionConflict(input.identity)
484
+ }
485
+
486
+ private insertQueued(record: ProjectionRunRecord): void {
487
+ const checkpoint = record.telemetryCheckpoint
488
+ this.db
489
+ .query(
490
+ `
491
+ INSERT INTO projection_runs (
492
+ project_id, id, execution_id, projection_id, projection_kind,
493
+ materialization_protocol, dataset_id, dataset_version_id,
494
+ dataset_version_created_at, ontology_revision, projection_revision, ownership_hash,
495
+ object_type_id, source_object_type_id, target_object_type_id, status, queued_at,
496
+ started_at, finished_at, attempt, execution_token, fixed_batch_size,
497
+ next_batch_ordinal, next_row_offset, input_exhausted, source_rows_read,
498
+ source_rows_skipped
499
+ ) VALUES (
500
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, NULL, NULL, 0, NULL,
501
+ ?, ?, ?, ?, 0, 0
502
+ )
503
+ `
504
+ )
505
+ .run(
506
+ record.projectId,
507
+ record.id,
508
+ record.executionId,
509
+ record.identity.projectionId,
510
+ record.identity.projectionKind,
511
+ record.identity.protocol,
512
+ record.identity.datasetVersion.datasetId,
513
+ record.identity.datasetVersion.versionId,
514
+ record.identity.datasetVersion.createdAt,
515
+ record.identity.ontologyRevision,
516
+ record.identity.projectionRevision,
517
+ record.identity.ownershipHash,
518
+ "objectTypeId" in record.target ? record.target.objectTypeId : null,
519
+ "sourceObjectTypeId" in record.target ? record.target.sourceObjectTypeId : null,
520
+ "sourceObjectTypeId" in record.target ? record.target.targetObjectTypeId : null,
521
+ record.queuedAt.toISOString(),
522
+ checkpoint?.fixedBatchSize ?? null,
523
+ checkpoint?.nextBatchOrdinal ?? null,
524
+ checkpoint?.nextRowOffset ?? null,
525
+ checkpoint === undefined ? null : checkpoint.inputExhausted ? 1 : 0
526
+ )
527
+ }
528
+
427
529
  private updateProgress(
428
530
  existing: DatabaseRow,
429
531
  progress: ProjectionRunProgress,
@@ -455,7 +557,7 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
455
557
  input: LockProjectionRunForMaterializationInput
456
558
  ): DatabaseRow {
457
559
  const row = this.requireRunning(input.projectId, input.id)
458
- assertProjectionRunExecution(restoreProjectionRunRow(row), input)
560
+ assertProjectionRunAttempt(restoreProjectionRunRow(row), input)
459
561
  return row
460
562
  }
461
563
 
@@ -494,6 +596,7 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
494
596
  const persisted: PersistedProjectionRunRecord = {
495
597
  id: row.id,
496
598
  projectId: row.project_id,
599
+ executionId: row.execution_id,
497
600
  projectionId: row.projection_id,
498
601
  projectionKind: row.projection_kind,
499
602
  protocol: row.materialization_protocol ?? undefined,
@@ -507,7 +610,8 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
507
610
  sourceObjectTypeId: row.source_object_type_id ?? undefined,
508
611
  targetObjectTypeId: row.target_object_type_id ?? undefined,
509
612
  status: row.status,
510
- startedAt: new Date(row.started_at),
613
+ queuedAt: new Date(row.queued_at),
614
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
511
615
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
512
616
  attempt: databaseSafeInteger(row.attempt, "attempt"),
513
617
  executionToken: row.execution_token ?? undefined,
@@ -528,7 +632,7 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
528
632
  sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
529
633
  sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
530
634
  },
531
- errorMessage: row.error_message ?? undefined,
635
+ error: row.error ?? undefined,
532
636
  }
533
637
  return restoreProjectionRun(persisted)
534
638
  }
@@ -561,6 +665,7 @@ function isUniqueConstraintError(error: unknown): boolean {
561
665
  interface DatabaseRow {
562
666
  project_id: string
563
667
  id: string
668
+ execution_id: string
564
669
  projection_id: string
565
670
  projection_kind: ProjectionKind
566
671
  materialization_protocol: "replacement" | "telemetry" | null
@@ -574,7 +679,8 @@ interface DatabaseRow {
574
679
  source_object_type_id: string | null
575
680
  target_object_type_id: string | null
576
681
  status: ProjectionRunStatus
577
- started_at: string
682
+ queued_at: string
683
+ started_at: string | null
578
684
  finished_at: string | null
579
685
  attempt: number
580
686
  execution_token: string | null
@@ -588,5 +694,11 @@ interface DatabaseRow {
588
694
  missing_target_first_seen_at: string | null
589
695
  source_rows_read: number
590
696
  source_rows_skipped: number
591
- error_message: string | null
697
+ error: string | null
698
+ }
699
+
700
+ function duplicateProjectionRun(input: { readonly projectId: string; readonly id: string }) {
701
+ return new ProjectionRunError(
702
+ `[SixbSqlite] Projection run '${input.id}' already exists for project '${input.projectId}'.`
703
+ )
592
704
  }
@@ -16,7 +16,10 @@ export function hasEmptyStatuses(input: { readonly statuses?: readonly unknown[]
16
16
  return input.statuses !== undefined && input.statuses.length === 0
17
17
  }
18
18
 
19
- type StartedAtExpression = "started_at" | "COALESCE(started_at, created_at)"
19
+ type StartedAtExpression =
20
+ | "started_at"
21
+ | "COALESCE(started_at, created_at)"
22
+ | "COALESCE(started_at, queued_at)"
20
23
 
21
24
  export function appendRunListFilters(
22
25
  whereClauses: string[],
@@ -68,9 +71,11 @@ export function queryRunList<TRow>(input: {
68
71
  const orderColumn =
69
72
  input.tableName === "agent_runs"
70
73
  ? "COALESCE(started_at, created_at)"
71
- : input.tableName === "workflow_agent_node_runs"
72
- ? "created_at"
73
- : "started_at"
74
+ : input.tableName === "sync_runs" || input.tableName === "pipeline_runs"
75
+ ? "COALESCE(started_at, queued_at)"
76
+ : input.tableName === "workflow_agent_node_runs"
77
+ ? "created_at"
78
+ : "started_at"
74
79
  const idColumn = input.tableName === "workflow_agent_node_runs" ? "node_run_id" : "id"
75
80
  let query = `
76
81
  SELECT * FROM ${input.tableName}