@sixb/pg 0.1.21 → 0.1.22
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 +1 -0
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +48 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +19 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +6 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.js +3720 -1594
- package/dist/index.js.map +37 -31
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +17 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +27 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +12 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +7 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +6 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- 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/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +8 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-artifact-storage.d.ts +8 -1
- package/dist/pg-artifact-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +400 -0
- package/src/agents/checkpoints.ts +8 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +175 -0
- package/src/agents/messages.ts +14 -7
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +154 -22
- package/src/agents/settings.ts +105 -0
- package/src/agents/threads.ts +356 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/users.ts +105 -43
- package/src/migrations/069-artifact-kinds.sql +4 -0
- package/src/migrations/070-artifact-state.sql +16 -0
- package/src/migrations/071-agent-thread-participants.sql +16 -0
- package/src/migrations/072-forked-context-checkpoints.sql +23 -0
- package/src/migrations/073-unique-values.sql +25 -0
- package/src/migrations/074-generated-media-usage.sql +21 -0
- package/src/migrations/075-model-call-files.sql +11 -0
- package/src/migrations/076-agent-memory.sql +33 -0
- package/src/migrations/077-artifact-threads.sql +21 -0
- package/src/migrations/078-agent-approvals.sql +58 -0
- package/src/migrations/079-agent-approval-resume.sql +20 -0
- package/src/migrations/080-subagent-threads.sql +145 -0
- package/src/migrations/081-subagent-questions.sql +6 -0
- package/src/migrations.ts +34 -0
- package/src/objects/conditional-reader.ts +44 -0
- package/src/objects/prepare-queries.ts +1 -1
- package/src/objects/query-compiler.ts +96 -33
- package/src/objects/query-indexes.ts +29 -10
- package/src/objects/read-scope.ts +140 -25
- package/src/objects/reader.ts +50 -6
- package/src/objects/scoped-reader.ts +101 -0
- package/src/objects/selected-reader.ts +16 -76
- package/src/objects/storage.ts +24 -5
- package/src/objects/text-count-index.ts +1 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +8 -0
- package/src/ontology-storage/materialization-writer.ts +76 -1
- package/src/ontology-storage/materializations.ts +20 -1
- package/src/ontology-storage/unique-values.ts +274 -0
- package/src/pg-ai-cost-storage.ts +62 -12
- package/src/pg-ai-usage-storage.ts +90 -1
- package/src/pg-artifact-storage.ts +205 -9
- package/src/pg-execution-storage.ts +31 -0
- package/src/storage-errors.ts +7 -0
- package/src/transactions.ts +1 -1
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
+
artifactStateBytes,
|
|
2
3
|
artifactStorageErrors,
|
|
4
|
+
checkArtifactStateWrite,
|
|
3
5
|
parseArtifactFrameGrantRow,
|
|
4
6
|
parseArtifactRow,
|
|
7
|
+
parseArtifactStateRow,
|
|
5
8
|
parseArtifactVersionRow,
|
|
6
9
|
} from "@sixb/core/internal/storage"
|
|
7
10
|
import type {
|
|
8
11
|
ArtifactFrameGrantRecord,
|
|
9
12
|
ArtifactRecord,
|
|
13
|
+
ArtifactStateEntry,
|
|
14
|
+
ArtifactStateKeyInput,
|
|
15
|
+
ArtifactStateScope,
|
|
10
16
|
ArtifactStorage,
|
|
11
17
|
ArtifactVersionInput,
|
|
12
18
|
ArtifactVersionRecord,
|
|
19
|
+
CopyArtifactInput,
|
|
13
20
|
CreateArtifactInput,
|
|
14
21
|
DeleteArtifactInput,
|
|
15
22
|
DeleteArtifactVersionsByOriginInput,
|
|
@@ -24,6 +31,7 @@ import type {
|
|
|
24
31
|
ListReferencingVersionsInput,
|
|
25
32
|
PublishArtifactVersionInput,
|
|
26
33
|
PublishedArtifactVersion,
|
|
34
|
+
SetArtifactStateInput,
|
|
27
35
|
} from "@sixb/core/storage"
|
|
28
36
|
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
29
37
|
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
@@ -51,6 +59,8 @@ const FRAME_GRANT_COLUMNS = `
|
|
|
51
59
|
targets, created_at AS "createdAt", expires_at AS "expiresAt"
|
|
52
60
|
`
|
|
53
61
|
|
|
62
|
+
const STATE_COLUMNS = `key, value, size_bytes AS "sizeBytes", updated_at AS "updatedAt"`
|
|
63
|
+
|
|
54
64
|
export class PgArtifactStorage implements ArtifactStorage {
|
|
55
65
|
constructor(private readonly sql: PgStoreClient) {}
|
|
56
66
|
|
|
@@ -80,7 +90,7 @@ export class PgArtifactStorage implements ArtifactStorage {
|
|
|
80
90
|
return runPgTransaction(this.sql, async (tx) => {
|
|
81
91
|
// The row lock serializes concurrent publishes of one artifact.
|
|
82
92
|
const [current] = await tx<PgRow[]>`
|
|
83
|
-
SELECT kind, next_version FROM artifacts
|
|
93
|
+
SELECT kind, next_version, latest_version FROM artifacts
|
|
84
94
|
WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
|
|
85
95
|
FOR UPDATE
|
|
86
96
|
`
|
|
@@ -89,6 +99,20 @@ export class PgArtifactStorage implements ArtifactStorage {
|
|
|
89
99
|
if (kind !== input.kind) {
|
|
90
100
|
throw artifactStorageErrors.kindMismatch(input.artifactId, kind, input.kind)
|
|
91
101
|
}
|
|
102
|
+
// Under the row lock: a publish from the same base after this one finds it moved.
|
|
103
|
+
const latestVersion = Number(current.latest_version)
|
|
104
|
+
if (input.baseVersion !== undefined && latestVersion !== input.baseVersion) {
|
|
105
|
+
const [row] = await tx<PgRow[]>`
|
|
106
|
+
SELECT ${tx.unsafe(VERSION_COLUMNS)} FROM artifact_versions
|
|
107
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
108
|
+
AND version = ${latestVersion}
|
|
109
|
+
`
|
|
110
|
+
const latest = parseArtifactVersionRow(row as PgRow)
|
|
111
|
+
throw artifactStorageErrors.moved(input.artifactId, input.baseVersion, {
|
|
112
|
+
version: latest.version,
|
|
113
|
+
author: latest.author,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
92
116
|
const version = await insertVersion(
|
|
93
117
|
tx,
|
|
94
118
|
input.projectId,
|
|
@@ -124,15 +148,15 @@ export class PgArtifactStorage implements ArtifactStorage {
|
|
|
124
148
|
where.push(`a.owner_principal_id = $${params.length + 1}`)
|
|
125
149
|
params.push(input.owner.id)
|
|
126
150
|
}
|
|
127
|
-
if (input.
|
|
151
|
+
if (input.threadId !== undefined) {
|
|
128
152
|
where.push(
|
|
129
153
|
`EXISTS (
|
|
130
|
-
SELECT 1 FROM
|
|
131
|
-
WHERE
|
|
132
|
-
AND
|
|
154
|
+
SELECT 1 FROM artifact_threads t
|
|
155
|
+
WHERE t.project_id = a.project_id AND t.artifact_id = a.id
|
|
156
|
+
AND t.thread_id = $${params.length + 1}
|
|
133
157
|
)`
|
|
134
158
|
)
|
|
135
|
-
params.push(input.
|
|
159
|
+
params.push(input.threadId)
|
|
136
160
|
}
|
|
137
161
|
if (input.kind !== undefined) {
|
|
138
162
|
where.push(`a.kind = $${params.length + 1}`)
|
|
@@ -163,6 +187,58 @@ export class PgArtifactStorage implements ArtifactStorage {
|
|
|
163
187
|
return row ? parseArtifactVersionRow(row) : null
|
|
164
188
|
}
|
|
165
189
|
|
|
190
|
+
async listThreadIds(input: GetArtifactInput): Promise<string[]> {
|
|
191
|
+
const rows = await this.sql<{ thread_id: string }[]>`
|
|
192
|
+
SELECT thread_id FROM artifact_threads
|
|
193
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.id}
|
|
194
|
+
ORDER BY thread_id COLLATE "C"
|
|
195
|
+
`
|
|
196
|
+
return rows.map((row) => row.thread_id)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async copy(input: CopyArtifactInput): Promise<ArtifactRecord> {
|
|
200
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
201
|
+
// The share lock keeps the source from being deleted or published to while it is copied, so
|
|
202
|
+
// the copy holds every version it has.
|
|
203
|
+
const [source] = await tx<PgRow[]>`
|
|
204
|
+
SELECT id FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.sourceId}
|
|
205
|
+
FOR SHARE
|
|
206
|
+
`
|
|
207
|
+
if (!source) throw artifactStorageErrors.notFound(input.sourceId)
|
|
208
|
+
|
|
209
|
+
const [artifact] = await tx<PgRow[]>`
|
|
210
|
+
INSERT INTO artifacts (
|
|
211
|
+
project_id, id, kind, title, owner_principal_type, owner_principal_id,
|
|
212
|
+
latest_version, next_version, created_at, updated_at
|
|
213
|
+
)
|
|
214
|
+
SELECT project_id, ${input.id}, kind, title, ${input.owner.type}, ${input.owner.id},
|
|
215
|
+
latest_version, next_version, created_at, updated_at
|
|
216
|
+
FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.sourceId}
|
|
217
|
+
ON CONFLICT (project_id, id) DO NOTHING
|
|
218
|
+
RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
|
|
219
|
+
`
|
|
220
|
+
if (!artifact) throw artifactStorageErrors.exists(input.id)
|
|
221
|
+
|
|
222
|
+
await tx`
|
|
223
|
+
INSERT INTO artifact_versions (
|
|
224
|
+
project_id, artifact_id, version, title, entry, files, outline, refs,
|
|
225
|
+
author_principal_type, author_principal_id, created_at
|
|
226
|
+
)
|
|
227
|
+
SELECT project_id, ${input.id}, version, title, entry, files, outline, refs,
|
|
228
|
+
author_principal_type, author_principal_id, created_at
|
|
229
|
+
FROM artifact_versions
|
|
230
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.sourceId}
|
|
231
|
+
`
|
|
232
|
+
await tx`
|
|
233
|
+
INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at, copied)
|
|
234
|
+
VALUES (
|
|
235
|
+
${input.projectId}, ${input.id}, ${input.threadId}, ${input.copiedAt ?? new Date()}, TRUE
|
|
236
|
+
)
|
|
237
|
+
`
|
|
238
|
+
return parseArtifactRow(artifact)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
166
242
|
async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
|
|
167
243
|
const [counted] = await this.sql<{ total: number }[]>`
|
|
168
244
|
SELECT COUNT(*)::int AS total FROM artifact_versions
|
|
@@ -241,8 +317,97 @@ export class PgArtifactStorage implements ArtifactStorage {
|
|
|
241
317
|
return rows.length
|
|
242
318
|
}
|
|
243
319
|
|
|
320
|
+
async getState(input: ArtifactStateKeyInput): Promise<ArtifactStateEntry | null> {
|
|
321
|
+
const [row] = await this.sql<PgRow[]>`
|
|
322
|
+
SELECT ${this.sql.unsafe(STATE_COLUMNS)} FROM artifact_state
|
|
323
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
324
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
325
|
+
AND key = ${input.key}
|
|
326
|
+
`
|
|
327
|
+
return row ? parseArtifactStateRow(row) : null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async setState(input: SetArtifactStateInput): Promise<void> {
|
|
331
|
+
await runPgTransaction(this.sql, async (tx) => {
|
|
332
|
+
// The key share lock keeps the artifact from being deleted under the write, without
|
|
333
|
+
// waiting on a publish.
|
|
334
|
+
const [artifact] = await tx<PgRow[]>`
|
|
335
|
+
SELECT 1 FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
|
|
336
|
+
FOR KEY SHARE
|
|
337
|
+
`
|
|
338
|
+
if (!artifact) throw artifactStorageErrors.notFound(input.artifactId)
|
|
339
|
+
// One reader's writes to one artifact take turns, so two at once cannot both fit under the
|
|
340
|
+
// limits; other readers' writes do not wait.
|
|
341
|
+
await tx`
|
|
342
|
+
SELECT pg_advisory_xact_lock(hashtextextended(${JSON.stringify([
|
|
343
|
+
"artifact_state",
|
|
344
|
+
input.projectId,
|
|
345
|
+
input.artifactId,
|
|
346
|
+
input.principal.type,
|
|
347
|
+
input.principal.id,
|
|
348
|
+
])}, 0))
|
|
349
|
+
`
|
|
350
|
+
const [others] = await tx<PgRow[]>`
|
|
351
|
+
SELECT count(*) AS keys, coalesce(sum(size_bytes), 0) AS bytes FROM artifact_state
|
|
352
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
353
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
354
|
+
AND key <> ${input.key}
|
|
355
|
+
`
|
|
356
|
+
const sizeBytes = artifactStateBytes(input.value)
|
|
357
|
+
checkArtifactStateWrite({
|
|
358
|
+
key: input.key,
|
|
359
|
+
valueBytes: sizeBytes,
|
|
360
|
+
others: { keys: Number(others?.keys ?? 0), bytes: Number(others?.bytes ?? 0) },
|
|
361
|
+
})
|
|
362
|
+
await tx`
|
|
363
|
+
INSERT INTO artifact_state (
|
|
364
|
+
project_id, artifact_id, principal_type, principal_id, key, value, size_bytes,
|
|
365
|
+
updated_at
|
|
366
|
+
) VALUES (
|
|
367
|
+
${input.projectId}, ${input.artifactId}, ${input.principal.type},
|
|
368
|
+
${input.principal.id}, ${input.key}, ${input.value}, ${sizeBytes}, ${input.updatedAt}
|
|
369
|
+
)
|
|
370
|
+
ON CONFLICT (project_id, artifact_id, principal_type, principal_id, key) DO UPDATE SET
|
|
371
|
+
value = excluded.value, size_bytes = excluded.size_bytes,
|
|
372
|
+
updated_at = excluded.updated_at
|
|
373
|
+
`
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async deleteState(input: ArtifactStateKeyInput): Promise<boolean> {
|
|
378
|
+
const rows = await this.sql`
|
|
379
|
+
DELETE FROM artifact_state
|
|
380
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
381
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
382
|
+
AND key = ${input.key}
|
|
383
|
+
RETURNING key
|
|
384
|
+
`
|
|
385
|
+
return rows.length > 0
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async listState(input: ArtifactStateScope): Promise<ArtifactStateEntry[]> {
|
|
389
|
+
// COLLATE "C": code-unit order, as SQLite and the in-memory store compare keys.
|
|
390
|
+
const rows = await this.sql<PgRow[]>`
|
|
391
|
+
SELECT ${this.sql.unsafe(STATE_COLUMNS)} FROM artifact_state
|
|
392
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
393
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
394
|
+
ORDER BY key COLLATE "C"
|
|
395
|
+
`
|
|
396
|
+
return rows.map(parseArtifactStateRow)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async clearState(input: ArtifactStateScope): Promise<number> {
|
|
400
|
+
const rows = await this.sql`
|
|
401
|
+
DELETE FROM artifact_state
|
|
402
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
403
|
+
AND principal_type = ${input.principal.type} AND principal_id = ${input.principal.id}
|
|
404
|
+
RETURNING key
|
|
405
|
+
`
|
|
406
|
+
return rows.length
|
|
407
|
+
}
|
|
408
|
+
|
|
244
409
|
async delete(input: DeleteArtifactInput): Promise<boolean> {
|
|
245
|
-
// Versions
|
|
410
|
+
// Versions, frame grants, and state go with the artifact through ON DELETE CASCADE.
|
|
246
411
|
const rows = await this.sql`
|
|
247
412
|
DELETE FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
248
413
|
RETURNING id
|
|
@@ -269,11 +434,24 @@ export class PgArtifactStorage implements ArtifactStorage {
|
|
|
269
434
|
`
|
|
270
435
|
const affected = locked.map((row) => row.id)
|
|
271
436
|
if (affected.length === 0) return 0
|
|
272
|
-
const deleted = await tx
|
|
437
|
+
const deleted = await tx<{ origin_thread_id: string }[]>`
|
|
273
438
|
DELETE FROM artifact_versions
|
|
274
439
|
WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
|
|
275
440
|
AND artifact_id IN ${tx(affected)}
|
|
276
|
-
RETURNING
|
|
441
|
+
RETURNING origin_thread_id
|
|
442
|
+
`
|
|
443
|
+
// A run publishes from one thread: its artifacts stay shared there only while a version
|
|
444
|
+
// from it remains, or when they were copied for it.
|
|
445
|
+
const threadId = deleted[0]?.origin_thread_id
|
|
446
|
+
await tx`
|
|
447
|
+
DELETE FROM artifact_threads t
|
|
448
|
+
WHERE t.project_id = ${input.projectId} AND t.artifact_id IN ${tx(affected)}
|
|
449
|
+
AND t.thread_id = ${threadId ?? null} AND NOT t.copied
|
|
450
|
+
AND NOT EXISTS (
|
|
451
|
+
SELECT 1 FROM artifact_versions v
|
|
452
|
+
WHERE v.project_id = t.project_id AND v.artifact_id = t.artifact_id
|
|
453
|
+
AND v.origin_thread_id = t.thread_id
|
|
454
|
+
)
|
|
277
455
|
`
|
|
278
456
|
// An artifact left without versions goes; the rest point at their newest remaining one.
|
|
279
457
|
await tx`
|
|
@@ -321,5 +499,23 @@ async function insertVersion(
|
|
|
321
499
|
)
|
|
322
500
|
RETURNING ${sql.unsafe(VERSION_COLUMNS)}
|
|
323
501
|
`
|
|
502
|
+
if (input.origin) {
|
|
503
|
+
await linkThread(sql, projectId, artifactId, input.origin.threadId, input.createdAt)
|
|
504
|
+
}
|
|
324
505
|
return parseArtifactVersionRow(row)
|
|
325
506
|
}
|
|
507
|
+
|
|
508
|
+
/** Shares an artifact in a thread, keeping when it was first shared there. */
|
|
509
|
+
async function linkThread(
|
|
510
|
+
sql: SQLClient,
|
|
511
|
+
projectId: string,
|
|
512
|
+
artifactId: string,
|
|
513
|
+
threadId: string,
|
|
514
|
+
sharedAt: Date
|
|
515
|
+
): Promise<void> {
|
|
516
|
+
await sql`
|
|
517
|
+
INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at)
|
|
518
|
+
VALUES (${projectId}, ${artifactId}, ${threadId}, ${sharedAt})
|
|
519
|
+
ON CONFLICT (project_id, artifact_id, thread_id) DO NOTHING
|
|
520
|
+
`
|
|
521
|
+
}
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
} from "@sixb/core/internal/execution-storage"
|
|
8
8
|
import type {
|
|
9
9
|
CreateExecutionInput,
|
|
10
|
+
ExecutionDescendant,
|
|
10
11
|
ExecutionRecord,
|
|
11
12
|
ExecutionStorage,
|
|
13
|
+
ListExecutionDescendantsInput,
|
|
12
14
|
ListSessionExecutionsInput,
|
|
13
15
|
SessionExecutions,
|
|
14
16
|
ShareSessionStorage,
|
|
@@ -163,6 +165,35 @@ export class PgExecutionStorage implements ExecutionStorage {
|
|
|
163
165
|
truncated: rows.length > input.limit,
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
|
|
169
|
+
async listDescendants(
|
|
170
|
+
input: ListExecutionDescendantsInput
|
|
171
|
+
): Promise<readonly ExecutionDescendant[]> {
|
|
172
|
+
if (input.executionIds.length === 0) return []
|
|
173
|
+
const ids = this.sql.array([...new Set(input.executionIds)])
|
|
174
|
+
|
|
175
|
+
// UNION, not UNION ALL: an execution asked about may sit below another one asked about, and
|
|
176
|
+
// the walk would otherwise reach what it caused twice.
|
|
177
|
+
const rows = await this.sql<{ id: string; parent_id: string }[]>`
|
|
178
|
+
WITH RECURSIVE tree(id, parent_id) AS (
|
|
179
|
+
SELECT child.id, child.parent_execution_id
|
|
180
|
+
FROM executions child
|
|
181
|
+
WHERE child.project_id = ${input.projectId}
|
|
182
|
+
AND child.parent_execution_id = ANY(${ids}::text[])
|
|
183
|
+
UNION
|
|
184
|
+
SELECT child.id, child.parent_execution_id
|
|
185
|
+
FROM tree
|
|
186
|
+
JOIN executions child
|
|
187
|
+
ON child.project_id = ${input.projectId}
|
|
188
|
+
AND child.parent_execution_id = tree.id
|
|
189
|
+
)
|
|
190
|
+
SELECT id, parent_id
|
|
191
|
+
FROM tree
|
|
192
|
+
WHERE id <> ALL(${ids}::text[])
|
|
193
|
+
ORDER BY id COLLATE "C"
|
|
194
|
+
`
|
|
195
|
+
return rows.map((row) => ({ id: row.id, parentId: row.parent_id }))
|
|
196
|
+
}
|
|
166
197
|
}
|
|
167
198
|
|
|
168
199
|
/** An `executions` row as its record, for any store that reads executions alongside its own rows. */
|
package/src/storage-errors.ts
CHANGED
|
@@ -81,6 +81,13 @@ export function isUniqueViolation(error: unknown): boolean {
|
|
|
81
81
|
return pgErrorCode(error) === UNIQUE_VIOLATION
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** True when the error is a unique violation of exactly the named constraint. */
|
|
85
|
+
export function isUniqueViolationOf(error: unknown, constraint: string): boolean {
|
|
86
|
+
if (!isUniqueViolation(error)) return false
|
|
87
|
+
|
|
88
|
+
return (error as { readonly constraint_name?: unknown }).constraint_name === constraint
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
/** True when the error is a PostgreSQL foreign-key violation (SQLSTATE 23503). */
|
|
85
92
|
export function isForeignKeyViolation(error: unknown): boolean {
|
|
86
93
|
return pgErrorCode(error) === FOREIGN_KEY_VIOLATION
|
package/src/transactions.ts
CHANGED
|
@@ -159,7 +159,7 @@ export async function runPgRepeatableReadTransaction<T>(
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
throw new Error(
|
|
162
|
-
'[SixbPg]
|
|
162
|
+
'[SixbPg] Scoped object reads cannot join an unverified PostgreSQL transaction. Use storage.transaction(..., { isolation: "serializable" }) when reading through tx.objects.'
|
|
163
163
|
)
|
|
164
164
|
}
|
|
165
165
|
|