@sixb/pg 0.1.8 → 0.1.10
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/dist/agents/rows.d.ts +0 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +562 -299
- package/dist/index.js.map +24 -22
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +3 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +2 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +2 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/ontology-storage/source-cleanup.d.ts +4 -0
- package/dist/ontology-storage/source-cleanup.d.ts.map +1 -0
- package/dist/ontology-storage/source-roots.d.ts +24 -0
- package/dist/ontology-storage/source-roots.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/pg-sync-run-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agents/rows.ts +0 -5
- package/src/agents/runs.ts +1 -6
- package/src/ai-cost-groups.ts +3 -0
- package/src/connector-connection-storage.ts +3 -3
- package/src/index.ts +4 -2
- package/src/migrations/037-execution-requester-groups.sql +21 -0
- package/src/migrations/038-outbox-publication-order.sql +5 -0
- package/src/migrations/039-projection-source-roots.sql +46 -0
- package/src/migrations/040-connector-optional-pkce.sql +3 -0
- package/src/migrations.ts +18 -0
- package/src/ontology-storage/materialization-session.ts +24 -8
- package/src/ontology-storage/materialization-state.ts +65 -86
- package/src/ontology-storage/materializations.ts +39 -13
- package/src/ontology-storage/shared.ts +10 -0
- package/src/ontology-storage/source-cleanup.ts +61 -0
- package/src/ontology-storage/source-roots.ts +251 -0
- package/src/ontology-storage/sources.ts +21 -57
- package/src/pg-action-run-storage.ts +6 -7
- package/src/pg-ai-cost-storage.ts +11 -2
- package/src/pg-ai-usage-storage.ts +1 -2
- package/src/pg-execution-storage.ts +6 -4
- package/src/pg-pipeline-run-storage.ts +2 -5
- package/src/pg-projection-run-storage.ts +8 -2
- package/src/pg-sync-run-storage.ts +2 -5
- package/src/pg-workflow-run-storage.ts +0 -5
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ProjectionEntityRef } from "@sixb/core/internal/materialization"
|
|
2
|
+
import {
|
|
3
|
+
MaterializationConflictError,
|
|
4
|
+
MaterializationValidationError,
|
|
5
|
+
projectionEntityKey,
|
|
6
|
+
} from "@sixb/core/internal/materialization"
|
|
7
|
+
import { sourceStageRoots, utf8SortKey } from "@sixb/core/internal/ontology-storage-provider"
|
|
8
|
+
import type { SourceActivationWrite, StageSourceRowsInput } from "@sixb/core/storage"
|
|
9
|
+
import type { SQLClient } from "../pg-client"
|
|
10
|
+
import type { PgOntologySourceAssertionRow } from "./shared"
|
|
11
|
+
import { jsonParameter, type PgOntologySourceRow } from "./shared"
|
|
12
|
+
|
|
13
|
+
export function replacementSourceRows(
|
|
14
|
+
sql: SQLClient,
|
|
15
|
+
input: {
|
|
16
|
+
readonly projectId: string
|
|
17
|
+
readonly sourceId: string
|
|
18
|
+
readonly materializationId: string
|
|
19
|
+
readonly incremental: boolean
|
|
20
|
+
}
|
|
21
|
+
) {
|
|
22
|
+
const roots = input.incremental
|
|
23
|
+
? sql`
|
|
24
|
+
SELECT roots.* FROM ontology_source_roots AS changed
|
|
25
|
+
CROSS JOIN LATERAL (
|
|
26
|
+
SELECT roots.* FROM ontology_source_roots AS roots
|
|
27
|
+
WHERE roots.project_id = changed.project_id AND roots.source_id = changed.source_id
|
|
28
|
+
AND roots.root_sort_key = changed.root_sort_key AND roots.active OFFSET 0
|
|
29
|
+
) AS roots
|
|
30
|
+
WHERE changed.project_id = ${input.projectId} AND changed.source_id = ${input.sourceId}
|
|
31
|
+
AND changed.materialization_id = ${input.materializationId}
|
|
32
|
+
`
|
|
33
|
+
: sql`SELECT * FROM ontology_source_roots WHERE project_id = ${input.projectId} AND source_id = ${input.sourceId} AND active`
|
|
34
|
+
return sql`
|
|
35
|
+
SELECT * FROM ontology_source_rows WHERE project_id = ${input.projectId}
|
|
36
|
+
AND source_id = ${input.sourceId} AND materialization_id = ${input.materializationId}
|
|
37
|
+
UNION ALL
|
|
38
|
+
SELECT rows.* FROM (${roots}) AS roots
|
|
39
|
+
CROSS JOIN LATERAL (
|
|
40
|
+
SELECT rows.* FROM ontology_source_rows AS rows WHERE rows.project_id = roots.project_id
|
|
41
|
+
AND rows.source_id = roots.source_id AND rows.materialization_id = roots.materialization_id
|
|
42
|
+
AND rows.root_sort_key = roots.root_sort_key OFFSET 0
|
|
43
|
+
) AS rows
|
|
44
|
+
`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function stageSourceRoots(
|
|
48
|
+
sql: SQLClient,
|
|
49
|
+
manifest: PgOntologySourceRow,
|
|
50
|
+
input: StageSourceRowsInput
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const roots = sourceStageRoots(manifest.projection_kind, input)
|
|
53
|
+
if (roots.length === 0) return
|
|
54
|
+
if (!manifest.base_materialization_id && roots.some((root) => root.deleted)) {
|
|
55
|
+
throw new MaterializationValidationError("Root deletions require a source delta base.")
|
|
56
|
+
}
|
|
57
|
+
const values = jsonParameter(
|
|
58
|
+
sql,
|
|
59
|
+
roots.map((root) => ({
|
|
60
|
+
root_sort_key: root.sortKey,
|
|
61
|
+
root_kind: root.root.kind,
|
|
62
|
+
root_key: JSON.parse(root.rootKey),
|
|
63
|
+
root: root.root,
|
|
64
|
+
staging_ordinal: root.stagingOrdinal,
|
|
65
|
+
deleted: root.deleted,
|
|
66
|
+
}))
|
|
67
|
+
)
|
|
68
|
+
const inserted = await sql<{ root_sort_key: string }[]>`
|
|
69
|
+
INSERT INTO ontology_source_roots (
|
|
70
|
+
project_id, source_id, materialization_id, root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
|
|
71
|
+
) SELECT ${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
|
|
72
|
+
root_sort_key, root_kind, root_key, root, staging_ordinal, deleted
|
|
73
|
+
FROM jsonb_to_recordset(${values}) AS input(
|
|
74
|
+
root_sort_key TEXT, root_kind TEXT, root_key JSONB, root JSONB, staging_ordinal BIGINT, deleted BOOLEAN)
|
|
75
|
+
ON CONFLICT DO NOTHING RETURNING root_sort_key
|
|
76
|
+
`
|
|
77
|
+
if (inserted.length === roots.length) return
|
|
78
|
+
const insertedKeys = new Set(inserted.map((row) => row.root_sort_key))
|
|
79
|
+
const skipped = roots.filter((root) => !insertedKeys.has(root.sortKey))
|
|
80
|
+
const stored = await sql<
|
|
81
|
+
{ root_sort_key: string; staging_ordinal: string | number; deleted: boolean }[]
|
|
82
|
+
>`
|
|
83
|
+
SELECT root_sort_key, staging_ordinal, deleted FROM ontology_source_roots
|
|
84
|
+
WHERE project_id = ${input.projectId} AND source_id = ${input.source.projectionId}
|
|
85
|
+
AND materialization_id = ${input.materializationId}
|
|
86
|
+
AND root_sort_key = ANY(${sql.array(skipped.map((root) => root.sortKey))}::text[])
|
|
87
|
+
`
|
|
88
|
+
const existing = new Map(stored.map((root) => [root.root_sort_key, root]))
|
|
89
|
+
if (
|
|
90
|
+
skipped.some((root) => {
|
|
91
|
+
const previous = existing.get(root.sortKey)
|
|
92
|
+
return (
|
|
93
|
+
!previous ||
|
|
94
|
+
Number(previous.staging_ordinal) !== root.stagingOrdinal ||
|
|
95
|
+
previous.deleted !== root.deleted
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
throw new MaterializationValidationError(
|
|
100
|
+
"Source materialization repeats root or stream ordinal with different content."
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function activateSourceRoots(
|
|
105
|
+
sql: SQLClient,
|
|
106
|
+
projectId: string,
|
|
107
|
+
candidate: PgOntologySourceRow,
|
|
108
|
+
activation: SourceActivationWrite
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
if (
|
|
111
|
+
candidate.base_materialization_id !== null &&
|
|
112
|
+
(candidate.base_materialization_id !== activation.expected.activeMaterializationId ||
|
|
113
|
+
candidate.base_commit_id !== activation.expected.lastCommitId)
|
|
114
|
+
)
|
|
115
|
+
throw new MaterializationConflictError(
|
|
116
|
+
"projection-fence",
|
|
117
|
+
"Source delta base changed before activation."
|
|
118
|
+
)
|
|
119
|
+
if (candidate.base_materialization_id !== null && Number(candidate.root_count) === 0) return
|
|
120
|
+
const affected =
|
|
121
|
+
candidate.base_materialization_id === null
|
|
122
|
+
? sql``
|
|
123
|
+
: sql`AND root_sort_key IN (
|
|
124
|
+
SELECT root_sort_key FROM ontology_source_roots
|
|
125
|
+
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
|
|
126
|
+
AND materialization_id = ${activation.materializationId}
|
|
127
|
+
)`
|
|
128
|
+
await sql`
|
|
129
|
+
UPDATE ontology_source_roots AS roots SET active = FALSE, retired_at = ${activation.updatedAt}
|
|
130
|
+
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId} AND active
|
|
131
|
+
${affected}
|
|
132
|
+
`
|
|
133
|
+
await sql`
|
|
134
|
+
UPDATE ontology_source_roots SET active = NOT deleted,
|
|
135
|
+
retired_at = CASE WHEN deleted THEN ${activation.updatedAt}::timestamptz ELSE NULL END
|
|
136
|
+
WHERE project_id = ${projectId} AND source_id = ${activation.source.projectionId}
|
|
137
|
+
AND materialization_id = ${activation.materializationId}
|
|
138
|
+
`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function assertSourceRootCoverage(
|
|
142
|
+
sql: SQLClient,
|
|
143
|
+
manifest: PgOntologySourceRow
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const [invalid] = await sql`
|
|
146
|
+
SELECT 1 FROM ontology_source_roots AS roots
|
|
147
|
+
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
148
|
+
AND materialization_id = ${manifest.materialization_id}
|
|
149
|
+
AND roots.deleted = EXISTS (
|
|
150
|
+
SELECT 1 FROM ontology_source_rows AS rows
|
|
151
|
+
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
152
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
153
|
+
) LIMIT 1
|
|
154
|
+
`
|
|
155
|
+
if (invalid)
|
|
156
|
+
throw new MaterializationValidationError(
|
|
157
|
+
"Source roots must have complete assertions or an explicit deletion, never both."
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function activeSourceRows(
|
|
162
|
+
sql: SQLClient,
|
|
163
|
+
projectId: string,
|
|
164
|
+
refs: readonly ProjectionEntityRef[]
|
|
165
|
+
) {
|
|
166
|
+
const keys = refs.flatMap((entity) => {
|
|
167
|
+
const entitySortKey = utf8SortKey(projectionEntityKey(entity))
|
|
168
|
+
const roots =
|
|
169
|
+
entity.kind === "object"
|
|
170
|
+
? [entitySortKey]
|
|
171
|
+
: [
|
|
172
|
+
entitySortKey,
|
|
173
|
+
utf8SortKey(projectionEntityKey({ kind: "object", ref: entity.ref.source })),
|
|
174
|
+
]
|
|
175
|
+
return roots.map((rootSortKey) => ({
|
|
176
|
+
root_sort_key: rootSortKey,
|
|
177
|
+
entity_sort_key: entitySortKey,
|
|
178
|
+
}))
|
|
179
|
+
})
|
|
180
|
+
return sql<PgOntologySourceAssertionRow[]>`
|
|
181
|
+
SELECT rows.* FROM jsonb_to_recordset(${jsonParameter(sql, keys)}) AS requested(root_sort_key TEXT, entity_sort_key TEXT)
|
|
182
|
+
JOIN ontology_source_roots AS roots ON roots.project_id = ${projectId}
|
|
183
|
+
AND roots.active AND roots.root_sort_key = requested.root_sort_key
|
|
184
|
+
JOIN ontology_source_rows AS rows ON rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
185
|
+
AND rows.materialization_id = roots.materialization_id AND rows.entity_sort_key = requested.entity_sort_key
|
|
186
|
+
AND rows.root_sort_key = roots.root_sort_key
|
|
187
|
+
`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function replacementAssertionRows(
|
|
191
|
+
sql: SQLClient,
|
|
192
|
+
input: {
|
|
193
|
+
projectId: string
|
|
194
|
+
sourceId: string
|
|
195
|
+
materializationId: string
|
|
196
|
+
incremental: boolean
|
|
197
|
+
includePrevious: boolean
|
|
198
|
+
refs: readonly ProjectionEntityRef[]
|
|
199
|
+
}
|
|
200
|
+
) {
|
|
201
|
+
const keys = input.refs.map((ref) => ({
|
|
202
|
+
entity_kind: ref.kind,
|
|
203
|
+
entity_key: JSON.parse(projectionEntityKey(ref)),
|
|
204
|
+
root_keys:
|
|
205
|
+
ref.kind === "object"
|
|
206
|
+
? [utf8SortKey(projectionEntityKey(ref))]
|
|
207
|
+
: [
|
|
208
|
+
utf8SortKey(projectionEntityKey(ref)),
|
|
209
|
+
utf8SortKey(projectionEntityKey({ kind: "object", ref: ref.ref.source })),
|
|
210
|
+
],
|
|
211
|
+
}))
|
|
212
|
+
const previous = !input.includePrevious
|
|
213
|
+
? sql``
|
|
214
|
+
: sql`UNION ALL
|
|
215
|
+
SELECT rows.* FROM (
|
|
216
|
+
SELECT roots.* FROM ontology_source_roots AS roots
|
|
217
|
+
WHERE roots.project_id = ${input.projectId} AND roots.source_id = ${input.sourceId}
|
|
218
|
+
AND roots.active AND roots.root_sort_key = ANY(requested.root_keys)
|
|
219
|
+
${
|
|
220
|
+
!input.incremental
|
|
221
|
+
? sql``
|
|
222
|
+
: sql`AND EXISTS (SELECT 1 FROM ontology_source_roots AS changed
|
|
223
|
+
WHERE changed.project_id = roots.project_id AND changed.source_id = roots.source_id
|
|
224
|
+
AND changed.materialization_id = ${input.materializationId} AND changed.root_sort_key = roots.root_sort_key)`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
OFFSET 0
|
|
228
|
+
) AS roots
|
|
229
|
+
CROSS JOIN LATERAL (
|
|
230
|
+
SELECT rows.* FROM ontology_source_rows AS rows
|
|
231
|
+
WHERE rows.project_id = roots.project_id AND rows.source_id = roots.source_id
|
|
232
|
+
AND rows.materialization_id = roots.materialization_id AND rows.root_sort_key = roots.root_sort_key
|
|
233
|
+
AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
234
|
+
OFFSET 0
|
|
235
|
+
) AS rows
|
|
236
|
+
`
|
|
237
|
+
// Keep each JSON request as an indexed identity lookup even with fresh/stale statistics.
|
|
238
|
+
// Flattening this LATERAL caused a cold 10k-root run to spend seconds on each state page.
|
|
239
|
+
return sql<PgOntologySourceAssertionRow[]>`
|
|
240
|
+
SELECT selected.* FROM jsonb_to_recordset(${jsonParameter(sql, keys)})
|
|
241
|
+
AS requested(entity_kind TEXT, entity_key JSONB, root_keys TEXT[])
|
|
242
|
+
CROSS JOIN LATERAL (
|
|
243
|
+
SELECT rows.* FROM ontology_source_rows AS rows
|
|
244
|
+
WHERE rows.project_id = ${input.projectId} AND rows.source_id = ${input.sourceId}
|
|
245
|
+
AND rows.materialization_id = ${input.materializationId}
|
|
246
|
+
AND rows.entity_kind = requested.entity_kind AND rows.entity_key = requested.entity_key
|
|
247
|
+
${previous}
|
|
248
|
+
OFFSET 0
|
|
249
|
+
) AS selected
|
|
250
|
+
`
|
|
251
|
+
}
|
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
sourceRecord,
|
|
50
50
|
toIsoString,
|
|
51
51
|
} from "./shared"
|
|
52
|
+
import { cleanupSourceVersions } from "./source-cleanup"
|
|
53
|
+
import { assertSourceRootCoverage, stageSourceRoots } from "./source-roots"
|
|
52
54
|
|
|
53
55
|
export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
54
56
|
constructor(private readonly runRootOperation: PgRootOperation) {}
|
|
@@ -97,14 +99,15 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
97
99
|
dataset_id, dataset_version_id, dataset_version_created_at,
|
|
98
100
|
projection_revision, ownership_hash, ontology_revision,
|
|
99
101
|
root_count, assertion_count, created_at, ready_at, activated_at,
|
|
100
|
-
terminal_at, last_commit_id, updated_at
|
|
102
|
+
terminal_at, last_commit_id, updated_at, base_materialization_id, base_commit_id
|
|
101
103
|
) VALUES (
|
|
102
104
|
${input.projectId}, ${input.source.projectionId}, ${input.materializationId},
|
|
103
105
|
${input.execution.projectionRunId}, ${input.projectionKind}, 'replacement',
|
|
104
106
|
'staging', ${input.execution.executionToken}, ${input.datasetVersion.datasetId},
|
|
105
107
|
${input.datasetVersion.versionId}, ${input.datasetVersion.createdAt},
|
|
106
108
|
${input.projectionRevision}, ${input.ownershipHash}, ${input.ontologyRevision},
|
|
107
|
-
NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt}
|
|
109
|
+
NULL, NULL, ${input.createdAt}, NULL, NULL, NULL, NULL, ${input.createdAt},
|
|
110
|
+
${input.base?.materializationId ?? null}, ${input.base?.lastCommitId ?? null}
|
|
108
111
|
)
|
|
109
112
|
ON CONFLICT DO NOTHING
|
|
110
113
|
RETURNING *
|
|
@@ -153,6 +156,7 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
153
156
|
const rows = sourceStageRows(manifest.projection_kind, input.rows)
|
|
154
157
|
const existing = await this.findStageRows(sql, input, rows)
|
|
155
158
|
const { pending, unchanged } = reconcileSourceStageRows(rows, existing)
|
|
159
|
+
await stageSourceRoots(sql, manifest, input)
|
|
156
160
|
await this.insertStageRows(sql, input, pending)
|
|
157
161
|
return { inserted: pending.length, unchanged }
|
|
158
162
|
})
|
|
@@ -193,6 +197,12 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
193
197
|
if (input.readyAt < toIsoString(manifest.created_at)) {
|
|
194
198
|
throw new MaterializationValidationError("Source readyAt cannot precede source createdAt.")
|
|
195
199
|
}
|
|
200
|
+
// A newly loaded run is absent from the planner statistics. Without refreshing them,
|
|
201
|
+
// PostgreSQL can scan the whole run for each identity instead of using its full key.
|
|
202
|
+
// Sparse deltas avoid this table-level sampling cost.
|
|
203
|
+
if (input.assertionCount >= 1_000 || input.rootCount >= 1_000) {
|
|
204
|
+
await sql`ANALYZE ontology_source_rows, ontology_source_roots`
|
|
205
|
+
}
|
|
196
206
|
await this.assertReadyState(sql, manifest, input.rootCount, input.assertionCount)
|
|
197
207
|
const rows = await sql<PgOntologySourceRow[]>`
|
|
198
208
|
UPDATE ontology_sources
|
|
@@ -247,59 +257,7 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
247
257
|
assertNonblank(input.projectId, "Terminal source cleanup project id")
|
|
248
258
|
assertTimestamp(input.terminalBefore, "Terminal source cleanup cutoff", true)
|
|
249
259
|
assertPositiveInteger(input.limit, "Terminal source cleanup limit")
|
|
250
|
-
|
|
251
|
-
SELECT * FROM ontology_sources
|
|
252
|
-
WHERE project_id = ${input.projectId}
|
|
253
|
-
AND status IN ('superseded', 'abandoned')
|
|
254
|
-
AND terminal_at < ${input.terminalBefore}
|
|
255
|
-
ORDER BY terminal_at, source_id, materialization_id
|
|
256
|
-
LIMIT ${input.limit}
|
|
257
|
-
FOR UPDATE SKIP LOCKED
|
|
258
|
-
`
|
|
259
|
-
let remaining = input.limit
|
|
260
|
-
let rowsDeleted = 0
|
|
261
|
-
let materializationsDeleted = 0
|
|
262
|
-
for (const manifest of manifests) {
|
|
263
|
-
if (remaining === 0) break
|
|
264
|
-
const deletedRows = await sql<{ readonly entity_sort_key: string }[]>`
|
|
265
|
-
WITH selected AS (
|
|
266
|
-
SELECT rows.ctid
|
|
267
|
-
FROM ontology_source_rows AS rows
|
|
268
|
-
WHERE rows.project_id = ${manifest.project_id}
|
|
269
|
-
AND rows.source_id = ${manifest.source_id}
|
|
270
|
-
AND rows.materialization_id = ${manifest.materialization_id}
|
|
271
|
-
ORDER BY rows.entity_sort_key
|
|
272
|
-
LIMIT ${remaining}
|
|
273
|
-
FOR UPDATE SKIP LOCKED
|
|
274
|
-
)
|
|
275
|
-
DELETE FROM ontology_source_rows AS rows
|
|
276
|
-
USING selected
|
|
277
|
-
WHERE rows.ctid = selected.ctid
|
|
278
|
-
RETURNING rows.entity_sort_key
|
|
279
|
-
`
|
|
280
|
-
rowsDeleted += deletedRows.length
|
|
281
|
-
remaining -= deletedRows.length
|
|
282
|
-
if (remaining === 0) break
|
|
283
|
-
const removed = await sql<{ readonly materialization_id: string }[]>`
|
|
284
|
-
DELETE FROM ontology_sources AS sources
|
|
285
|
-
WHERE sources.project_id = ${manifest.project_id}
|
|
286
|
-
AND sources.source_id = ${manifest.source_id}
|
|
287
|
-
AND sources.materialization_id = ${manifest.materialization_id}
|
|
288
|
-
AND sources.status IN ('superseded', 'abandoned')
|
|
289
|
-
AND sources.terminal_at = ${manifest.terminal_at}
|
|
290
|
-
AND sources.terminal_at < ${input.terminalBefore}
|
|
291
|
-
AND NOT EXISTS (
|
|
292
|
-
SELECT 1 FROM ontology_source_rows AS rows
|
|
293
|
-
WHERE rows.project_id = sources.project_id
|
|
294
|
-
AND rows.source_id = sources.source_id
|
|
295
|
-
AND rows.materialization_id = sources.materialization_id
|
|
296
|
-
)
|
|
297
|
-
RETURNING sources.materialization_id
|
|
298
|
-
`
|
|
299
|
-
materializationsDeleted += removed.length
|
|
300
|
-
remaining -= removed.length
|
|
301
|
-
}
|
|
302
|
-
return { rowsDeleted, materializationsDeleted }
|
|
260
|
+
return cleanupSourceVersions(sql, input)
|
|
303
261
|
})
|
|
304
262
|
}
|
|
305
263
|
|
|
@@ -408,6 +366,9 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
408
366
|
`Source materialization '${manifest.materialization_id}' cannot transition to 'abandoned'.`
|
|
409
367
|
)
|
|
410
368
|
}
|
|
369
|
+
await sql`UPDATE ontology_source_roots SET retired_at = ${abandonedAt}
|
|
370
|
+
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
371
|
+
AND materialization_id = ${manifest.materialization_id} AND NOT active`
|
|
411
372
|
return sourceRecord(rows[0])
|
|
412
373
|
}
|
|
413
374
|
|
|
@@ -426,10 +387,12 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
426
387
|
readonly max_ordinal: number | string | null
|
|
427
388
|
}[]
|
|
428
389
|
>`
|
|
429
|
-
SELECT
|
|
390
|
+
SELECT (SELECT COUNT(*) FROM ontology_source_rows
|
|
391
|
+
WHERE project_id = ${manifest.project_id} AND source_id = ${manifest.source_id}
|
|
392
|
+
AND materialization_id = ${manifest.materialization_id}) AS assertions, COUNT(*) AS roots,
|
|
430
393
|
COUNT(DISTINCT staging_ordinal) AS ordinals,
|
|
431
394
|
MIN(staging_ordinal) AS min_ordinal, MAX(staging_ordinal) AS max_ordinal
|
|
432
|
-
FROM
|
|
395
|
+
FROM ontology_source_roots
|
|
433
396
|
WHERE project_id = ${manifest.project_id}
|
|
434
397
|
AND source_id = ${manifest.source_id}
|
|
435
398
|
AND materialization_id = ${manifest.materialization_id}
|
|
@@ -449,6 +412,7 @@ export class PgOntologySourceStorage implements OntologySourceStorage {
|
|
|
449
412
|
"Source ready counts do not match the staged roots and assertions."
|
|
450
413
|
)
|
|
451
414
|
}
|
|
415
|
+
await assertSourceRootCoverage(sql, manifest)
|
|
452
416
|
|
|
453
417
|
const [invalid] =
|
|
454
418
|
manifest.projection_kind === "link"
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
serializeActionRunFailure,
|
|
5
5
|
} from "@sixb/core/internal/action-run-storage"
|
|
6
6
|
import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider"
|
|
7
|
+
import {
|
|
8
|
+
actionRunPhaseRecordsEqual,
|
|
9
|
+
canRequeueActionRunAfterEnqueueFailure,
|
|
10
|
+
finishActionRunPhase,
|
|
11
|
+
} from "@sixb/core/internal/storage"
|
|
7
12
|
import type {
|
|
8
13
|
ActionRunEffectsRecord,
|
|
9
14
|
ActionRunFailure,
|
|
@@ -23,13 +28,7 @@ import type {
|
|
|
23
28
|
RecordActionWritebackInput,
|
|
24
29
|
StartActionRunInput,
|
|
25
30
|
} from "@sixb/core/storage"
|
|
26
|
-
import {
|
|
27
|
-
ActionRunError,
|
|
28
|
-
actionRunPhaseRecordsEqual,
|
|
29
|
-
canRequeueActionRunAfterEnqueueFailure,
|
|
30
|
-
finishActionRunPhase,
|
|
31
|
-
isTerminalActionRun,
|
|
32
|
-
} from "@sixb/core/storage"
|
|
31
|
+
import { ActionRunError, isTerminalActionRun } from "@sixb/core/storage"
|
|
33
32
|
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
34
33
|
import { isUniqueViolation } from "./storage-errors"
|
|
35
34
|
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
aiModelCallCostDetails,
|
|
5
5
|
aiModelCallCostMatchesUsage,
|
|
6
6
|
buildAiAccountingOverviewFromFragments,
|
|
7
|
+
directModelCallAttribution,
|
|
7
8
|
normalizeAiAccountingQuery,
|
|
8
9
|
normalizeAiModelCallAccountingQuery,
|
|
9
10
|
normalizeAiModelCallCostRecord,
|
|
@@ -117,11 +118,14 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
117
118
|
ELSE NULL
|
|
118
119
|
END AS attribution_agent_kind,
|
|
119
120
|
workflow_node.node_id AS attribution_agent_step_id,
|
|
120
|
-
workflow_node.workflow_id
|
|
121
|
+
COALESCE(workflow_node.workflow_id,
|
|
122
|
+
CASE WHEN execution.executor_kind = 'workflow' THEN execution.authority_primitive_id END
|
|
123
|
+
) AS attribution_workflow_id,
|
|
121
124
|
cost.status AS cost_status,
|
|
122
125
|
cost.currency AS cost_currency,
|
|
123
126
|
cost.amount_nanos AS cost_amount_nanos
|
|
124
127
|
FROM ai_model_call_usage AS usage
|
|
128
|
+
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
125
129
|
LEFT JOIN agent_runs AS direct_agent
|
|
126
130
|
ON direct_agent.project_id = usage.project_id
|
|
127
131
|
AND direct_agent.execution_id = usage.execution_id
|
|
@@ -251,6 +255,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
251
255
|
workflow_agent.node_run_id AS attribution_node_run_id,
|
|
252
256
|
workflow_node.workflow_id AS attribution_workflow_id,
|
|
253
257
|
workflow_node.workflow_run_id AS attribution_workflow_run_id,
|
|
258
|
+
execution.executor_kind, execution.executor_id, execution.authority_primitive_id AS primitive_id,
|
|
254
259
|
COALESCE((
|
|
255
260
|
SELECT jsonb_agg(groups.group_id ORDER BY groups.group_id)
|
|
256
261
|
FROM ai_model_call_usage_groups AS groups
|
|
@@ -265,6 +270,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
265
270
|
cost.details AS cost_details,
|
|
266
271
|
cost.rated_at AS cost_rated_at
|
|
267
272
|
FROM ai_model_call_usage AS usage
|
|
273
|
+
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
268
274
|
LEFT JOIN agent_runs AS direct_agent
|
|
269
275
|
ON direct_agent.project_id = usage.project_id
|
|
270
276
|
AND direct_agent.execution_id = usage.execution_id
|
|
@@ -342,6 +348,9 @@ interface UsageRow {
|
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
interface JoinedRow extends UsageRow {
|
|
351
|
+
readonly executor_kind: string | null
|
|
352
|
+
readonly executor_id: string | null
|
|
353
|
+
readonly primitive_id: string | null
|
|
345
354
|
readonly attribution_kind: "agent" | "subagent" | "workflowAgent" | null
|
|
346
355
|
readonly attribution_agent_step_id: string | null
|
|
347
356
|
readonly attribution_agent_run_id: string | null
|
|
@@ -491,7 +500,7 @@ function accountingItemFromRow(row: JoinedRow): AiAccountingRecordSetItem {
|
|
|
491
500
|
const cost = row.cost_status === null ? undefined : costFromJoinedRow(row)
|
|
492
501
|
const attribution =
|
|
493
502
|
row.attribution_kind === null
|
|
494
|
-
?
|
|
503
|
+
? directModelCallAttribution(row.executor_kind, row.executor_id, row.primitive_id)
|
|
495
504
|
: row.attribution_kind === "agent"
|
|
496
505
|
? {
|
|
497
506
|
kind: "agent" as const,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertAiUsageExecutionId, normalizeAiModelCallRecord } from "@sixb/core/internal/storage"
|
|
1
2
|
import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
|
|
2
3
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
3
4
|
import {
|
|
@@ -7,9 +8,7 @@ import {
|
|
|
7
8
|
type AiUsageStorage,
|
|
8
9
|
AiUsageStorageError,
|
|
9
10
|
aggregateAiModelCallUsage,
|
|
10
|
-
assertAiUsageExecutionId,
|
|
11
11
|
type GetLatestAiModelCallForExecutionInput,
|
|
12
|
-
normalizeAiModelCallRecord,
|
|
13
12
|
type RecordAiModelCallInput,
|
|
14
13
|
type RecordAiModelCallResult,
|
|
15
14
|
type SummarizeAiUsageExecutionInput,
|
|
@@ -2,8 +2,7 @@ import {
|
|
|
2
2
|
type ExecutionStorageRow,
|
|
3
3
|
executionRecordFromStorageRow,
|
|
4
4
|
executionRecordToStorageRow,
|
|
5
|
-
|
|
6
|
-
validateExecutionRecordReferences,
|
|
5
|
+
prepareExecutionRecord,
|
|
7
6
|
} from "@sixb/core/internal/execution-storage"
|
|
8
7
|
import type {
|
|
9
8
|
CreateExecutionInput,
|
|
@@ -24,8 +23,7 @@ export class PgExecutionStorage implements ExecutionStorage {
|
|
|
24
23
|
) {}
|
|
25
24
|
|
|
26
25
|
async create(input: CreateExecutionInput): Promise<ExecutionRecord> {
|
|
27
|
-
const record =
|
|
28
|
-
await validateExecutionRecordReferences(record, {
|
|
26
|
+
const record = await prepareExecutionRecord(input, {
|
|
29
27
|
auth: this.auth,
|
|
30
28
|
getExecution: (params) => this.getById(params),
|
|
31
29
|
getShareSession: (params) => this.shareSessions.getById(params),
|
|
@@ -43,6 +41,7 @@ export class PgExecutionStorage implements ExecutionStorage {
|
|
|
43
41
|
source_id,
|
|
44
42
|
requested_by_user_id,
|
|
45
43
|
requested_by_service_account_id,
|
|
44
|
+
requester_group_ids,
|
|
46
45
|
correlation_id,
|
|
47
46
|
parent_execution_id,
|
|
48
47
|
authority_kind,
|
|
@@ -66,6 +65,7 @@ export class PgExecutionStorage implements ExecutionStorage {
|
|
|
66
65
|
${row.sourceId},
|
|
67
66
|
${row.requestedByUserId},
|
|
68
67
|
${row.requestedByServiceAccountId},
|
|
68
|
+
${JSON.stringify(row.requesterGroupIds)}::text::jsonb,
|
|
69
69
|
${row.correlationId},
|
|
70
70
|
${row.parentExecutionId},
|
|
71
71
|
${row.authorityKind},
|
|
@@ -123,6 +123,7 @@ function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
|
|
|
123
123
|
sourceId: row.source_id,
|
|
124
124
|
requestedByUserId: row.requested_by_user_id,
|
|
125
125
|
requestedByServiceAccountId: row.requested_by_service_account_id,
|
|
126
|
+
requesterGroupIds: row.requester_group_ids,
|
|
126
127
|
correlationId: row.correlation_id,
|
|
127
128
|
parentExecutionId: row.parent_execution_id,
|
|
128
129
|
authorityKind: row.authority_kind,
|
|
@@ -149,6 +150,7 @@ interface PgExecutionRow {
|
|
|
149
150
|
readonly source_id: string
|
|
150
151
|
readonly requested_by_user_id: string | null
|
|
151
152
|
readonly requested_by_service_account_id: string | null
|
|
153
|
+
readonly requester_group_ids: string[]
|
|
152
154
|
readonly correlation_id: string
|
|
153
155
|
readonly parent_execution_id: string | null
|
|
154
156
|
readonly authority_kind: ExecutionStorageRow["authorityKind"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JsonValue } from "@sixb/core"
|
|
2
2
|
import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
3
3
|
import { assertPipelineRunExecution } from "@sixb/core/internal/pipeline-run-storage-provider"
|
|
4
|
+
import { canRequeuePipelineRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
|
|
4
5
|
import type { DatasetVersionRef } from "@sixb/core/lake-storage"
|
|
5
6
|
import type {
|
|
6
7
|
ExecutionStorage,
|
|
@@ -19,11 +20,7 @@ import type {
|
|
|
19
20
|
StartPipelineRunInput,
|
|
20
21
|
StartPipelineStepRunInput,
|
|
21
22
|
} from "@sixb/core/storage"
|
|
22
|
-
import {
|
|
23
|
-
canRequeuePipelineRunAfterEnqueueFailure,
|
|
24
|
-
PIPELINE_RUN_FAILURE_CODES,
|
|
25
|
-
PipelineRunError,
|
|
26
|
-
} from "@sixb/core/storage"
|
|
23
|
+
import { PIPELINE_RUN_FAILURE_CODES, PipelineRunError } from "@sixb/core/storage"
|
|
27
24
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
28
25
|
import type { SqlParameter } from "./pg-client"
|
|
29
26
|
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
|
|
@@ -102,7 +102,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
102
102
|
input_exhausted = ${input.fixedBatchSize === undefined ? null : false},
|
|
103
103
|
missing_target_object_type_id = ${null}, missing_target_object_id = ${null},
|
|
104
104
|
missing_target_batch_ordinal = ${null}, missing_target_first_seen_at = ${null},
|
|
105
|
-
source_rows_read = ${0}, source_rows_skipped = ${0}, error = ${null}
|
|
105
|
+
source_rows_read = ${0}, source_rows_skipped = ${0}, source_changes_read = ${null}, error = ${null}
|
|
106
106
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
107
107
|
AND status = ${"failed"} AND error->>'code' = ${"queue.enqueue_failed"}
|
|
108
108
|
RETURNING *
|
|
@@ -216,7 +216,8 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
216
216
|
UPDATE projection_runs
|
|
217
217
|
SET
|
|
218
218
|
source_rows_read = ${progress.sourceRowsRead},
|
|
219
|
-
source_rows_skipped = ${progress.sourceRowsSkipped}
|
|
219
|
+
source_rows_skipped = ${progress.sourceRowsSkipped},
|
|
220
|
+
source_changes_read = ${progress.sourceChangesRead ?? null}
|
|
220
221
|
WHERE project_id = ${input.projectId}
|
|
221
222
|
AND id = ${input.id}
|
|
222
223
|
AND status = ${"running"}
|
|
@@ -241,6 +242,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
241
242
|
execution_token = ${null},
|
|
242
243
|
source_rows_read = ${plan.progress.sourceRowsRead},
|
|
243
244
|
source_rows_skipped = ${plan.progress.sourceRowsSkipped},
|
|
245
|
+
source_changes_read = ${plan.progress.sourceChangesRead ?? null},
|
|
244
246
|
input_exhausted = ${plan.inputExhausted ?? existingRow.input_exhausted},
|
|
245
247
|
error = ${plan.error === undefined ? null : serializeSixbFailure(plan.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
246
248
|
WHERE project_id = ${input.projectId}
|
|
@@ -529,6 +531,9 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
529
531
|
progress: {
|
|
530
532
|
sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
|
|
531
533
|
sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
|
|
534
|
+
...(row.source_changes_read == null
|
|
535
|
+
? {}
|
|
536
|
+
: { sourceChangesRead: databaseSafeInteger(row.source_changes_read, "sourceChangesRead") }),
|
|
532
537
|
},
|
|
533
538
|
error: row.error ?? undefined,
|
|
534
539
|
}
|
|
@@ -584,6 +589,7 @@ interface DatabaseRow {
|
|
|
584
589
|
missing_target_first_seen_at: Date | string | null
|
|
585
590
|
source_rows_read: number | string
|
|
586
591
|
source_rows_skipped: number | string
|
|
592
|
+
source_changes_read: number | string | null
|
|
587
593
|
error: JsonValue | null
|
|
588
594
|
}
|
|
589
595
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JsonValue } from "@sixb/core"
|
|
2
2
|
import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
3
|
+
import { canRequeueSyncRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
|
|
3
4
|
import { assertSyncRunExecution } from "@sixb/core/internal/sync-run-storage-provider"
|
|
4
5
|
import type {
|
|
5
6
|
ExecutionStorage,
|
|
@@ -13,11 +14,7 @@ import type {
|
|
|
13
14
|
SyncRunRecord,
|
|
14
15
|
SyncRunStorage,
|
|
15
16
|
} from "@sixb/core/storage"
|
|
16
|
-
import {
|
|
17
|
-
canRequeueSyncRunAfterEnqueueFailure,
|
|
18
|
-
SYNC_RUN_FAILURE_CODES,
|
|
19
|
-
SyncRunError,
|
|
20
|
-
} from "@sixb/core/storage"
|
|
17
|
+
import { SYNC_RUN_FAILURE_CODES, SyncRunError } from "@sixb/core/storage"
|
|
21
18
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
22
19
|
import type { SqlParameter } from "./pg-client"
|
|
23
20
|
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { JsonValue } from "@sixb/core"
|
|
2
|
-
import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
|
|
3
2
|
import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
4
3
|
import {
|
|
5
4
|
assertWorkflowAgentNodeRunExecution,
|
|
@@ -83,7 +82,6 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
|
|
|
83
82
|
input,
|
|
84
83
|
queued_at,
|
|
85
84
|
started_at,
|
|
86
|
-
requester_group_ids,
|
|
87
85
|
attempt
|
|
88
86
|
) VALUES (
|
|
89
87
|
${input.projectId},
|
|
@@ -94,7 +92,6 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
|
|
|
94
92
|
${serializeRecord(input.input)}::text::jsonb,
|
|
95
93
|
${queuedAt},
|
|
96
94
|
${queuedAt},
|
|
97
|
-
${JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds))}::text::jsonb,
|
|
98
95
|
${0}
|
|
99
96
|
)
|
|
100
97
|
RETURNING *
|
|
@@ -822,7 +819,6 @@ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord
|
|
|
822
819
|
startedAt: new Date(row.started_at),
|
|
823
820
|
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
824
821
|
error: row.error === null ? undefined : parseSixbFailure(row.error, WORKFLOW_RUN_FAILURE_CODES),
|
|
825
|
-
requesterGroupIds: parseJson(row.requester_group_ids),
|
|
826
822
|
attempt: Number(row.attempt),
|
|
827
823
|
...(row.execution_token && row.execution_queue_lease_expires_at
|
|
828
824
|
? {
|
|
@@ -883,7 +879,6 @@ interface WorkflowRunDatabaseRow {
|
|
|
883
879
|
started_at: Date | string
|
|
884
880
|
finished_at: Date | string | null
|
|
885
881
|
error: JsonValue | null
|
|
886
|
-
requester_group_ids: string[] | string
|
|
887
882
|
attempt: number | string
|
|
888
883
|
execution_token: string | null
|
|
889
884
|
execution_queue_lease_expires_at: Date | string | null
|