@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- 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
|
+
}
|