@sixb/pg 0.1.1 → 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.
- package/README.md +7 -6
- package/dist/agents/agent-storage.d.ts +2 -1
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +4 -11
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/auth-storage/service-accounts.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +0 -4
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/connector-connection-storage.d.ts +9 -0
- package/dist/connector-connection-storage.d.ts.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3086 -730
- package/dist/index.js.map +28 -25
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +0 -2
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/outbox.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +3 -6
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +3 -2
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +16 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +14 -0
- package/dist/pg-execution-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +5 -2
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
- package/dist/pg-projection-run-storage.d.ts +5 -2
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/pg-sync-run-storage.d.ts +5 -2
- package/dist/pg-sync-run-storage.d.ts.map +1 -1
- package/dist/pg-webhook-run-storage.d.ts +5 -2
- package/dist/pg-webhook-run-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +5 -3
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/run-list-query.d.ts +1 -1
- package/dist/run-list-query.d.ts.map +1 -1
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +3 -2
- package/src/agents/rows.ts +12 -43
- package/src/agents/runs.ts +29 -15
- package/src/auth-storage/service-accounts.ts +14 -15
- package/src/auth-storage/shared.ts +0 -8
- package/src/connector-connection-storage.ts +760 -0
- package/src/index.ts +32 -15
- package/src/latest-run-query.ts +7 -1
- package/src/migrations/004-executions.sql +140 -0
- package/src/migrations/005-workflow-executions.sql +10 -0
- package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
- package/src/migrations/007-split-overrides.sql +153 -0
- package/src/migrations/008-action-executions.sql +7 -0
- package/src/migrations/009-agent-executions.sql +21 -0
- package/src/migrations/010-ai-usage-accounting-foundation.sql +76 -0
- package/src/migrations/011-sync-failure-record.sql +23 -0
- package/src/migrations/012-pipeline-failure-record.sql +47 -0
- package/src/migrations/013-workflow-failure-record.sql +45 -0
- package/src/migrations/014-agent-failure-record.sql +56 -0
- package/src/migrations/015-projection-failure-record.sql +20 -0
- package/src/migrations/016-webhook-run-failure-record.sql +19 -0
- package/src/migrations/017-action-failure-record.sql +65 -0
- package/src/migrations/018-ontology-outbox-failure-record.sql +29 -0
- package/src/migrations/019-webhook-delivery-failure-record.sql +27 -0
- package/src/migrations/020-drop-run-usage-projections.sql +8 -0
- package/src/migrations/021-sync-pipeline-executions.sql +50 -0
- package/src/migrations/022-projection-executions.sql +115 -0
- package/src/migrations/023-webhook-executions.sql +30 -0
- package/src/migrations/024-ontology-commit-executions.sql +12 -0
- package/src/migrations/025-connector-connections.sql +231 -0
- package/src/migrations.ts +72 -0
- package/src/ontology-storage/materialization-session.ts +25 -28
- package/src/ontology-storage/materialization-state.ts +159 -43
- package/src/ontology-storage/materialization-writer.ts +146 -75
- package/src/ontology-storage/materializations.ts +3 -1
- package/src/ontology-storage/outbox.ts +9 -2
- package/src/ontology-storage/shared.ts +10 -20
- package/src/pg-action-run-storage.ts +43 -67
- package/src/pg-ai-usage-storage.ts +290 -0
- package/src/pg-execution-storage.ts +147 -0
- package/src/pg-pipeline-run-storage.ts +107 -41
- package/src/pg-projection-run-storage.ts +159 -91
- package/src/pg-sync-run-storage.ts +100 -28
- package/src/pg-webhook-run-storage.ts +89 -57
- package/src/pg-workflow-run-storage.ts +97 -130
- package/src/run-list-query.ts +9 -4
- package/src/testing.ts +18 -0
- package/dist/pg-webhook-delivery-storage.d.ts +0 -17
- package/dist/pg-webhook-delivery-storage.d.ts.map +0 -1
- package/src/pg-webhook-delivery-storage.ts +0 -180
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto"
|
|
2
|
+
import type { JsonValue } from "@sixb/core"
|
|
3
|
+
import { serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
2
4
|
import {
|
|
3
5
|
advanceProjectionTelemetry,
|
|
4
6
|
assertGenericProgressDoesNotAdvanceTelemetry,
|
|
5
7
|
assertProjectionMissingTarget,
|
|
6
|
-
|
|
8
|
+
assertProjectionRunAttempt,
|
|
9
|
+
assertProjectionRunDurableExecution,
|
|
7
10
|
assertProjectionRunListWindow,
|
|
8
11
|
assertProjectionRunNonEmpty,
|
|
12
|
+
assertProjectionRunQueueInput,
|
|
9
13
|
assertProjectionRunRunning,
|
|
10
14
|
assertProjectionRunStartInput,
|
|
15
|
+
canRequeueProjectionRunAfterEnqueueFailure,
|
|
11
16
|
createProjectionRunClaim,
|
|
17
|
+
createProjectionRunRecord,
|
|
18
|
+
failProjectionRunEnqueue,
|
|
12
19
|
immutableDatasetVersionConflict,
|
|
13
20
|
mergeProjectionRunProgress,
|
|
14
21
|
type PersistedProjectionRunRecord,
|
|
@@ -16,7 +23,6 @@ import {
|
|
|
16
23
|
planProjectionRunReclaim,
|
|
17
24
|
projectionRunNotFound,
|
|
18
25
|
publicProjectionRunRecord,
|
|
19
|
-
requireProjectionRunExecutionToken,
|
|
20
26
|
requireTelemetryProjectionRun,
|
|
21
27
|
restoreProjectionRun,
|
|
22
28
|
type StoredProjectionRunRecord,
|
|
@@ -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,
|
|
@@ -35,21 +43,34 @@ import type {
|
|
|
35
43
|
ProjectionRunRecord,
|
|
36
44
|
ProjectionRunStatus,
|
|
37
45
|
ProjectionRunStorage,
|
|
46
|
+
QueueProjectionRunInput,
|
|
38
47
|
RecordProjectionMissingTargetInput,
|
|
39
48
|
StartOrReclaimProjectionRunInput,
|
|
40
49
|
TelemetryProjectionRunRecord,
|
|
41
50
|
UpdateProjectionRunInput,
|
|
42
51
|
} from "@sixb/core/storage"
|
|
43
|
-
import { ProjectionRunError } from "@sixb/core/storage"
|
|
52
|
+
import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage"
|
|
44
53
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
45
54
|
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
46
55
|
import { lockAdvisoryKeys, type PgStoreClient, runPgTransaction } from "./transactions"
|
|
47
56
|
|
|
48
57
|
export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
49
|
-
constructor(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly sql: PgStoreClient,
|
|
60
|
+
private readonly executions: ExecutionStorage
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
async queue(input: QueueProjectionRunInput): Promise<ProjectionRunRecord> {
|
|
64
|
+
assertProjectionRunQueueInput(input)
|
|
65
|
+
await assertProjectionRunDurableExecution({
|
|
66
|
+
executions: this.executions,
|
|
67
|
+
projectId: input.projectId,
|
|
68
|
+
executionId: input.executionId,
|
|
69
|
+
runId: input.id,
|
|
70
|
+
projectionId: input.identity.projectionId,
|
|
71
|
+
datasetId: input.identity.datasetVersion.datasetId,
|
|
72
|
+
datasetVersionId: input.identity.datasetVersion.versionId,
|
|
73
|
+
})
|
|
53
74
|
|
|
54
75
|
return runPgTransaction(this.sql, async (tx) => {
|
|
55
76
|
await lockAdvisoryKeys(tx, [
|
|
@@ -60,98 +81,119 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
60
81
|
input.identity.datasetVersion.versionId
|
|
61
82
|
),
|
|
62
83
|
])
|
|
63
|
-
|
|
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
|
-
|
|
84
|
+
await assertDatasetVersionIsImmutable(tx, input)
|
|
77
85
|
const [existingRow] = await tx<DatabaseRow[]>`
|
|
78
86
|
SELECT * FROM projection_runs
|
|
79
87
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
80
88
|
FOR UPDATE
|
|
81
89
|
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const [updated] = await tx<DatabaseRow[]>`
|
|
90
|
+
if (existingRow) {
|
|
91
|
+
if (
|
|
92
|
+
!canRequeueProjectionRunAfterEnqueueFailure(rowToProjectionRunRecord(existingRow), input)
|
|
93
|
+
) {
|
|
94
|
+
throw duplicateProjectionRun(input)
|
|
95
|
+
}
|
|
96
|
+
const [requeued] = await tx<DatabaseRow[]>`
|
|
91
97
|
UPDATE projection_runs
|
|
92
|
-
SET
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
SET status = ${"queued"}, queued_at = ${input.queuedAt ?? new Date()},
|
|
99
|
+
started_at = ${null}, finished_at = ${null}, attempt = ${0}, execution_token = ${null},
|
|
100
|
+
next_batch_ordinal = ${input.fixedBatchSize === undefined ? null : 0},
|
|
101
|
+
next_row_offset = ${input.fixedBatchSize === undefined ? null : 0},
|
|
102
|
+
input_exhausted = ${input.fixedBatchSize === undefined ? null : false},
|
|
103
|
+
missing_target_object_type_id = ${null}, missing_target_object_id = ${null},
|
|
104
|
+
missing_target_batch_ordinal = ${null}, missing_target_first_seen_at = ${null},
|
|
105
|
+
source_rows_read = ${0}, source_rows_skipped = ${0}, error = ${null}
|
|
106
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
107
|
+
AND status = ${"failed"} AND error->>'code' = ${"queue.enqueue_failed"}
|
|
97
108
|
RETURNING *
|
|
98
109
|
`
|
|
99
|
-
if (!
|
|
100
|
-
return
|
|
110
|
+
if (!requeued) throw duplicateProjectionRun(input)
|
|
111
|
+
return rowToProjectionRunRecord(requeued)
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
const record = createProjectionRunRecord(input)
|
|
115
|
+
const checkpoint = record.telemetryCheckpoint
|
|
103
116
|
const [inserted] = await tx<DatabaseRow[]>`
|
|
104
117
|
INSERT INTO projection_runs (
|
|
105
|
-
project_id,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
118
|
+
project_id, id, execution_id, projection_id, projection_kind,
|
|
119
|
+
materialization_protocol, dataset_id, dataset_version_id,
|
|
120
|
+
dataset_version_created_at, ontology_revision, projection_revision, ownership_hash,
|
|
121
|
+
object_type_id, source_object_type_id, target_object_type_id, status, queued_at,
|
|
122
|
+
started_at, finished_at, attempt, execution_token, fixed_batch_size,
|
|
123
|
+
next_batch_ordinal, next_row_offset, input_exhausted, source_rows_read,
|
|
124
|
+
source_rows_skipped
|
|
127
125
|
) VALUES (
|
|
128
|
-
${
|
|
129
|
-
${
|
|
130
|
-
${
|
|
131
|
-
${
|
|
132
|
-
${
|
|
133
|
-
${
|
|
134
|
-
${"objectTypeId" in
|
|
135
|
-
${"sourceObjectTypeId" in
|
|
136
|
-
${"sourceObjectTypeId" in
|
|
137
|
-
${"
|
|
138
|
-
${
|
|
139
|
-
${
|
|
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}
|
|
126
|
+
${record.projectId}, ${record.id}, ${record.executionId},
|
|
127
|
+
${record.identity.projectionId}, ${record.identity.projectionKind},
|
|
128
|
+
${record.identity.protocol}, ${record.identity.datasetVersion.datasetId},
|
|
129
|
+
${record.identity.datasetVersion.versionId}, ${record.identity.datasetVersion.createdAt},
|
|
130
|
+
${record.identity.ontologyRevision}, ${record.identity.projectionRevision},
|
|
131
|
+
${record.identity.ownershipHash},
|
|
132
|
+
${"objectTypeId" in record.target ? record.target.objectTypeId : null},
|
|
133
|
+
${"sourceObjectTypeId" in record.target ? record.target.sourceObjectTypeId : null},
|
|
134
|
+
${"sourceObjectTypeId" in record.target ? record.target.targetObjectTypeId : null},
|
|
135
|
+
${"queued"}, ${record.queuedAt}, ${null}, ${null}, ${0}, ${null},
|
|
136
|
+
${checkpoint?.fixedBatchSize ?? null}, ${checkpoint?.nextBatchOrdinal ?? null},
|
|
137
|
+
${checkpoint?.nextRowOffset ?? null}, ${checkpoint?.inputExhausted ?? null}, ${0}, ${0}
|
|
150
138
|
)
|
|
151
139
|
RETURNING *
|
|
152
140
|
`
|
|
141
|
+
return rowToProjectionRunRecord(inserted)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async startOrReclaim(input: StartOrReclaimProjectionRunInput): Promise<ProjectionRunClaim> {
|
|
146
|
+
assertProjectionRunStartInput(input)
|
|
153
147
|
|
|
154
|
-
|
|
148
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
149
|
+
await lockAdvisoryKeys(tx, [projectionRunLockKey(input.projectId, input.id)])
|
|
150
|
+
const [existingRow] = await tx<DatabaseRow[]>`
|
|
151
|
+
SELECT * FROM projection_runs
|
|
152
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
153
|
+
FOR UPDATE
|
|
154
|
+
`
|
|
155
|
+
if (!existingRow) throw projectionRunNotFound(input.projectId, input.id)
|
|
156
|
+
const existing = restoreProjectionRunRow(existingRow)
|
|
157
|
+
const { attempt } = planProjectionRunReclaim(existing, input)
|
|
158
|
+
const executionToken = createFreshExecutionToken(existing.executionToken)
|
|
159
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
160
|
+
UPDATE projection_runs
|
|
161
|
+
SET status = ${"running"}, started_at = COALESCE(started_at, ${input.startedAt ?? new Date()}),
|
|
162
|
+
finished_at = ${null}, attempt = ${attempt}, execution_token = ${executionToken},
|
|
163
|
+
error = ${null}
|
|
164
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
165
|
+
AND (
|
|
166
|
+
(status = ${"queued"} AND execution_token IS NULL)
|
|
167
|
+
OR (status = ${"running"} AND execution_token = ${existing.executionToken ?? null})
|
|
168
|
+
)
|
|
169
|
+
RETURNING *
|
|
170
|
+
`
|
|
171
|
+
if (!updated) throw staleProjectionRunExecution(input.id)
|
|
172
|
+
return projectionRunClaim(updated)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async failEnqueue(input: FailProjectionRunEnqueueInput): Promise<ProjectionRunRecord> {
|
|
177
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
178
|
+
const [existingRow] = await tx<DatabaseRow[]>`
|
|
179
|
+
SELECT * FROM projection_runs
|
|
180
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
181
|
+
FOR UPDATE
|
|
182
|
+
`
|
|
183
|
+
if (!existingRow) throw projectionRunNotFound(input.projectId, input.id)
|
|
184
|
+
const failed = failProjectionRunEnqueue(restoreProjectionRunRow(existingRow), input)
|
|
185
|
+
if (!failed.error) {
|
|
186
|
+
throw new ProjectionRunError(`[SixbPg] Projection run '${input.id}' has no failure.`)
|
|
187
|
+
}
|
|
188
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
189
|
+
UPDATE projection_runs
|
|
190
|
+
SET status = ${"failed"}, finished_at = ${failed.finishedAt ?? new Date()},
|
|
191
|
+
error = ${serializeSixbFailure(failed.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
192
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"queued"}
|
|
193
|
+
RETURNING *
|
|
194
|
+
`
|
|
195
|
+
if (!updated) throw duplicateProjectionRun(input)
|
|
196
|
+
return rowToProjectionRunRecord(updated)
|
|
155
197
|
})
|
|
156
198
|
}
|
|
157
199
|
|
|
@@ -200,7 +242,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
200
242
|
source_rows_read = ${plan.progress.sourceRowsRead},
|
|
201
243
|
source_rows_skipped = ${plan.progress.sourceRowsSkipped},
|
|
202
244
|
input_exhausted = ${plan.inputExhausted ?? existingRow.input_exhausted},
|
|
203
|
-
|
|
245
|
+
error = ${plan.error === undefined ? null : serializeSixbFailure(plan.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
204
246
|
WHERE project_id = ${input.projectId}
|
|
205
247
|
AND id = ${input.id}
|
|
206
248
|
AND status = ${"running"}
|
|
@@ -328,11 +370,11 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
328
370
|
params.push(...input.statuses)
|
|
329
371
|
}
|
|
330
372
|
if (input.startedAfter) {
|
|
331
|
-
whereClauses.push(`started_at >= $${index++}`)
|
|
373
|
+
whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`)
|
|
332
374
|
params.push(input.startedAfter)
|
|
333
375
|
}
|
|
334
376
|
if (input.startedBefore) {
|
|
335
|
-
whereClauses.push(`started_at <= $${index++}`)
|
|
377
|
+
whereClauses.push(`COALESCE(started_at, queued_at) <= $${index++}`)
|
|
336
378
|
params.push(input.startedBefore)
|
|
337
379
|
}
|
|
338
380
|
|
|
@@ -351,7 +393,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
351
393
|
let query = `
|
|
352
394
|
SELECT * FROM projection_runs
|
|
353
395
|
${where}
|
|
354
|
-
ORDER BY started_at ${order}, id ${order}
|
|
396
|
+
ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
|
|
355
397
|
`
|
|
356
398
|
if (input.limit !== undefined) {
|
|
357
399
|
query += ` LIMIT $${index++} OFFSET $${index++}`
|
|
@@ -388,7 +430,7 @@ async function requireMaterializationExecution(
|
|
|
388
430
|
input: LockProjectionRunForMaterializationInput
|
|
389
431
|
): Promise<DatabaseRow> {
|
|
390
432
|
const row = await requireRunning(sql, input.projectId, input.id)
|
|
391
|
-
|
|
433
|
+
assertProjectionRunAttempt(restoreProjectionRunRow(row), input)
|
|
392
434
|
return row
|
|
393
435
|
}
|
|
394
436
|
|
|
@@ -406,6 +448,22 @@ async function requireRunning(sql: SQLClient, projectId: string, id: string): Pr
|
|
|
406
448
|
return row
|
|
407
449
|
}
|
|
408
450
|
|
|
451
|
+
async function assertDatasetVersionIsImmutable(
|
|
452
|
+
sql: SQLClient,
|
|
453
|
+
input: QueueProjectionRunInput
|
|
454
|
+
): Promise<void> {
|
|
455
|
+
const [conflict] = await sql<{ readonly id: string }[]>`
|
|
456
|
+
SELECT id FROM projection_runs
|
|
457
|
+
WHERE project_id = ${input.projectId}
|
|
458
|
+
AND id <> ${input.id}
|
|
459
|
+
AND dataset_id = ${input.identity.datasetVersion.datasetId}
|
|
460
|
+
AND dataset_version_id = ${input.identity.datasetVersion.versionId}
|
|
461
|
+
AND dataset_version_created_at <> ${input.identity.datasetVersion.createdAt}
|
|
462
|
+
LIMIT 1
|
|
463
|
+
`
|
|
464
|
+
if (conflict) throw immutableDatasetVersionConflict(input.identity)
|
|
465
|
+
}
|
|
466
|
+
|
|
409
467
|
function createFreshExecutionToken(previous: string | undefined): string {
|
|
410
468
|
let token = randomUUID()
|
|
411
469
|
while (token === previous) token = randomUUID()
|
|
@@ -436,6 +494,7 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
436
494
|
const persisted: PersistedProjectionRunRecord = {
|
|
437
495
|
id: row.id,
|
|
438
496
|
projectId: row.project_id,
|
|
497
|
+
executionId: row.execution_id,
|
|
439
498
|
projectionId: row.projection_id,
|
|
440
499
|
projectionKind: row.projection_kind,
|
|
441
500
|
protocol: row.materialization_protocol ?? undefined,
|
|
@@ -449,7 +508,8 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
449
508
|
sourceObjectTypeId: row.source_object_type_id ?? undefined,
|
|
450
509
|
targetObjectTypeId: row.target_object_type_id ?? undefined,
|
|
451
510
|
status: row.status,
|
|
452
|
-
|
|
511
|
+
queuedAt: new Date(row.queued_at),
|
|
512
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
453
513
|
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
454
514
|
attempt: databaseSafeInteger(row.attempt, "attempt"),
|
|
455
515
|
executionToken: row.execution_token ?? undefined,
|
|
@@ -470,7 +530,7 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
470
530
|
sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
|
|
471
531
|
sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
|
|
472
532
|
},
|
|
473
|
-
|
|
533
|
+
error: row.error ?? undefined,
|
|
474
534
|
}
|
|
475
535
|
return restoreProjectionRun(persisted)
|
|
476
536
|
}
|
|
@@ -495,6 +555,7 @@ function databaseSafeInteger(value: number | string | null, fieldName: string):
|
|
|
495
555
|
interface DatabaseRow {
|
|
496
556
|
project_id: string
|
|
497
557
|
id: string
|
|
558
|
+
execution_id: string
|
|
498
559
|
projection_id: string
|
|
499
560
|
projection_kind: ProjectionKind
|
|
500
561
|
dataset_id: string
|
|
@@ -503,7 +564,8 @@ interface DatabaseRow {
|
|
|
503
564
|
source_object_type_id: string | null
|
|
504
565
|
target_object_type_id: string | null
|
|
505
566
|
status: ProjectionRunStatus
|
|
506
|
-
|
|
567
|
+
queued_at: Date | string
|
|
568
|
+
started_at: Date | string | null
|
|
507
569
|
finished_at: Date | string | null
|
|
508
570
|
attempt: number | string
|
|
509
571
|
execution_token: string | null
|
|
@@ -522,5 +584,11 @@ interface DatabaseRow {
|
|
|
522
584
|
missing_target_first_seen_at: Date | string | null
|
|
523
585
|
source_rows_read: number | string
|
|
524
586
|
source_rows_skipped: number | string
|
|
525
|
-
|
|
587
|
+
error: JsonValue | null
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function duplicateProjectionRun(input: { readonly projectId: string; readonly id: string }) {
|
|
591
|
+
return new ProjectionRunError(
|
|
592
|
+
`[SixbPg] Projection run '${input.id}' already exists for project '${input.projectId}'.`
|
|
593
|
+
)
|
|
526
594
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { JsonValue } from "@sixb/core"
|
|
2
|
+
import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
3
|
+
import { assertSyncRunExecution } from "@sixb/core/internal/sync-run-storage-provider"
|
|
2
4
|
import type {
|
|
5
|
+
ExecutionStorage,
|
|
3
6
|
FinishSyncRunInput,
|
|
4
7
|
ListLatestSyncRunsInput,
|
|
5
8
|
ListLatestSyncRunsResult,
|
|
6
9
|
ListSyncRunsInput,
|
|
7
10
|
ListSyncRunsResult,
|
|
11
|
+
QueueSyncRunInput,
|
|
8
12
|
StartSyncRunInput,
|
|
9
|
-
SyncRunFailure,
|
|
10
13
|
SyncRunRecord,
|
|
11
14
|
SyncRunStorage,
|
|
12
15
|
} from "@sixb/core/storage"
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
canRequeueSyncRunAfterEnqueueFailure,
|
|
18
|
+
SYNC_RUN_FAILURE_CODES,
|
|
19
|
+
SyncRunError,
|
|
20
|
+
} from "@sixb/core/storage"
|
|
14
21
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
15
22
|
import type { SqlParameter } from "./pg-client"
|
|
16
23
|
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
|
|
@@ -18,29 +25,43 @@ import { isUniqueViolation } from "./storage-errors"
|
|
|
18
25
|
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
19
26
|
|
|
20
27
|
export class PgSyncRunStorage implements SyncRunStorage {
|
|
21
|
-
constructor(
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly sql: PgStoreClient,
|
|
30
|
+
private readonly executions: ExecutionStorage
|
|
31
|
+
) {}
|
|
22
32
|
|
|
23
|
-
async
|
|
33
|
+
async queue(input: QueueSyncRunInput): Promise<SyncRunRecord> {
|
|
34
|
+
await assertSyncRunExecution({
|
|
35
|
+
executions: this.executions,
|
|
36
|
+
projectId: input.projectId,
|
|
37
|
+
executionId: input.executionId,
|
|
38
|
+
runId: input.id,
|
|
39
|
+
syncId: input.syncId,
|
|
40
|
+
})
|
|
24
41
|
try {
|
|
25
42
|
const [row] = await this.sql<DatabaseRow[]>`
|
|
26
43
|
INSERT INTO sync_runs (
|
|
27
44
|
project_id,
|
|
28
45
|
id,
|
|
46
|
+
execution_id,
|
|
29
47
|
sync_id,
|
|
30
48
|
dataset_id,
|
|
31
49
|
mode,
|
|
32
50
|
status,
|
|
51
|
+
queued_at,
|
|
33
52
|
started_at,
|
|
34
53
|
expected_latest_version_id,
|
|
35
54
|
commit_message
|
|
36
55
|
) VALUES (
|
|
37
56
|
${input.projectId},
|
|
38
57
|
${input.id},
|
|
58
|
+
${input.executionId},
|
|
39
59
|
${input.syncId},
|
|
40
60
|
${input.datasetId},
|
|
41
61
|
${input.mode},
|
|
42
|
-
${"
|
|
43
|
-
${input.
|
|
62
|
+
${"queued"},
|
|
63
|
+
${input.queuedAt ?? new Date()},
|
|
64
|
+
${null},
|
|
44
65
|
${input.expectedLatestVersionId ?? null},
|
|
45
66
|
${input.commitMessage ?? null}
|
|
46
67
|
)
|
|
@@ -50,13 +71,57 @@ export class PgSyncRunStorage implements SyncRunStorage {
|
|
|
50
71
|
return rowToSyncRunRecord(row)
|
|
51
72
|
} catch (error) {
|
|
52
73
|
if (isUniqueViolation(error)) {
|
|
74
|
+
return this.requeueAfterEnqueueFailure(input)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw error
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async requeueAfterEnqueueFailure(input: QueueSyncRunInput): Promise<SyncRunRecord> {
|
|
82
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
83
|
+
const [existing] = await tx<DatabaseRow[]>`
|
|
84
|
+
SELECT *, checkpoint IS NOT NULL AS checkpoint_present FROM sync_runs
|
|
85
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
86
|
+
FOR UPDATE
|
|
87
|
+
`
|
|
88
|
+
if (!existing || !canRequeueSyncRunAfterEnqueueFailure(rowToSyncRunRecord(existing), input)) {
|
|
53
89
|
throw new SyncRunError(
|
|
54
90
|
`[SixbPg] Sync run '${input.id}' already exists for project '${input.projectId}'.`
|
|
55
91
|
)
|
|
56
92
|
}
|
|
57
93
|
|
|
58
|
-
|
|
94
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
95
|
+
UPDATE sync_runs
|
|
96
|
+
SET status = ${"queued"}, queued_at = ${input.queuedAt ?? new Date()},
|
|
97
|
+
started_at = ${null}, finished_at = ${null}, rows_read = ${null},
|
|
98
|
+
output_version_id = ${null}, error = ${null}, checkpoint = ${null}
|
|
99
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
100
|
+
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
101
|
+
`
|
|
102
|
+
return rowToSyncRunRecord(updated)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
|
|
107
|
+
const [updated] = await this.sql<DatabaseRow[]>`
|
|
108
|
+
UPDATE sync_runs
|
|
109
|
+
SET status = ${"running"}, started_at = ${input.startedAt ?? new Date()},
|
|
110
|
+
error = ${null}
|
|
111
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"queued"}
|
|
112
|
+
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
113
|
+
`
|
|
114
|
+
if (updated) return rowToSyncRunRecord(updated)
|
|
115
|
+
|
|
116
|
+
const existing = await this.getById({ projectId: input.projectId, id: input.id })
|
|
117
|
+
if (!existing) {
|
|
118
|
+
throw new SyncRunError(
|
|
119
|
+
`[SixbPg] Sync run '${input.id}' not found for project '${input.projectId}'.`
|
|
120
|
+
)
|
|
59
121
|
}
|
|
122
|
+
throw new SyncRunError(
|
|
123
|
+
`[SixbPg] Sync run '${input.id}' cannot start from status '${existing.status}'.`
|
|
124
|
+
)
|
|
60
125
|
}
|
|
61
126
|
|
|
62
127
|
async finish(input: FinishSyncRunInput): Promise<SyncRunRecord> {
|
|
@@ -72,6 +137,17 @@ export class PgSyncRunStorage implements SyncRunStorage {
|
|
|
72
137
|
)
|
|
73
138
|
}
|
|
74
139
|
|
|
140
|
+
const enqueueFailure =
|
|
141
|
+
input.status === "failed" && input.error.code === "queue.enqueue_failed"
|
|
142
|
+
if (
|
|
143
|
+
(enqueueFailure && existing.status !== "queued") ||
|
|
144
|
+
(!enqueueFailure && existing.status !== "running")
|
|
145
|
+
) {
|
|
146
|
+
throw new SyncRunError(
|
|
147
|
+
`[SixbPg] Sync run '${input.id}' cannot finish from status '${existing.status}'.`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
75
151
|
if (input.status === "succeeded") {
|
|
76
152
|
if (input.output && input.output.datasetId !== existing.dataset_id) {
|
|
77
153
|
throw new SyncRunError(
|
|
@@ -94,8 +170,7 @@ export class PgSyncRunStorage implements SyncRunStorage {
|
|
|
94
170
|
finished_at = ${input.finishedAt ?? new Date()},
|
|
95
171
|
rows_read = ${input.rowsRead},
|
|
96
172
|
output_version_id = ${input.output?.versionId ?? null},
|
|
97
|
-
|
|
98
|
-
error_message = ${null},
|
|
173
|
+
error = ${null},
|
|
99
174
|
checkpoint = ${serializeCheckpoint(input.checkpoint)}::text::jsonb
|
|
100
175
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
101
176
|
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
@@ -107,8 +182,7 @@ export class PgSyncRunStorage implements SyncRunStorage {
|
|
|
107
182
|
finished_at = ${input.finishedAt ?? new Date()},
|
|
108
183
|
rows_read = ${input.rowsRead ?? existing.rows_read ?? null},
|
|
109
184
|
output_version_id = ${null},
|
|
110
|
-
|
|
111
|
-
error_message = ${input.error?.message ?? null},
|
|
185
|
+
error = ${input.error === undefined ? null : serializeSixbFailure(input.error, SYNC_RUN_FAILURE_CODES)}::text::jsonb,
|
|
112
186
|
checkpoint = ${null}
|
|
113
187
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
114
188
|
RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
|
|
@@ -156,7 +230,13 @@ export class PgSyncRunStorage implements SyncRunStorage {
|
|
|
156
230
|
params.push(input.datasetId)
|
|
157
231
|
}
|
|
158
232
|
|
|
159
|
-
index = appendRunListFilters(
|
|
233
|
+
index = appendRunListFilters(
|
|
234
|
+
whereClauses,
|
|
235
|
+
params,
|
|
236
|
+
index,
|
|
237
|
+
input,
|
|
238
|
+
"COALESCE(started_at, queued_at)"
|
|
239
|
+
)
|
|
160
240
|
|
|
161
241
|
const { rows, total, hasMore } = await queryRunList<DatabaseRow>({
|
|
162
242
|
sql: this.sql,
|
|
@@ -198,26 +278,17 @@ function serializeCheckpoint(checkpoint: JsonValue | undefined): string | null {
|
|
|
198
278
|
return checkpoint !== undefined ? JSON.stringify(checkpoint) : null
|
|
199
279
|
}
|
|
200
280
|
|
|
201
|
-
function toSyncRunFailure(row: DatabaseRow): SyncRunFailure | undefined {
|
|
202
|
-
if (!row.error_message) {
|
|
203
|
-
return undefined
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
name: row.error_name ?? undefined,
|
|
208
|
-
message: row.error_message,
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
281
|
function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
|
|
213
282
|
return {
|
|
214
283
|
id: row.id,
|
|
215
284
|
projectId: row.project_id,
|
|
285
|
+
executionId: row.execution_id,
|
|
216
286
|
syncId: row.sync_id,
|
|
217
287
|
datasetId: row.dataset_id,
|
|
218
288
|
mode: row.mode,
|
|
219
289
|
status: row.status,
|
|
220
|
-
|
|
290
|
+
queuedAt: new Date(row.queued_at),
|
|
291
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
221
292
|
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
222
293
|
rowsRead: row.rows_read != null ? Number(row.rows_read) : undefined,
|
|
223
294
|
output: row.output_version_id
|
|
@@ -228,7 +299,7 @@ function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
|
|
|
228
299
|
: undefined,
|
|
229
300
|
expectedLatestVersionId: row.expected_latest_version_id ?? undefined,
|
|
230
301
|
commitMessage: row.commit_message ?? undefined,
|
|
231
|
-
error:
|
|
302
|
+
error: row.error === null ? undefined : parseSixbFailure(row.error, SYNC_RUN_FAILURE_CODES),
|
|
232
303
|
checkpoint: hasCheckpoint(row) ? row.checkpoint : undefined,
|
|
233
304
|
}
|
|
234
305
|
}
|
|
@@ -246,18 +317,19 @@ function hasCheckpoint(row: DatabaseRow): boolean {
|
|
|
246
317
|
interface DatabaseRow {
|
|
247
318
|
project_id: string
|
|
248
319
|
id: string
|
|
320
|
+
execution_id: string
|
|
249
321
|
sync_id: string
|
|
250
322
|
dataset_id: string
|
|
251
323
|
mode: SyncRunRecord["mode"]
|
|
252
324
|
status: SyncRunRecord["status"]
|
|
253
|
-
|
|
325
|
+
queued_at: Date | string
|
|
326
|
+
started_at: Date | string | null
|
|
254
327
|
finished_at: Date | string | null
|
|
255
328
|
rows_read: number | string | null
|
|
256
329
|
output_version_id: string | null
|
|
257
330
|
expected_latest_version_id: string | null
|
|
258
331
|
commit_message: string | null
|
|
259
|
-
|
|
260
|
-
error_message: string | null
|
|
332
|
+
error: JsonValue | null
|
|
261
333
|
checkpoint: JsonValue | null
|
|
262
334
|
checkpoint_present: boolean | number | string
|
|
263
335
|
}
|