@sixb/pg 0.1.3 → 0.1.5
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/checkpoints.d.ts +4 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/index.js +182 -6
- package/dist/index.js.map +7 -7
- package/dist/pg-ai-usage-storage.d.ts +2 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-object-storage.d.ts +4 -3
- package/dist/pg-object-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/checkpoints.ts +29 -0
- package/src/pg-ai-usage-storage.ts +15 -0
- package/src/pg-object-query-compiler.ts +81 -1
- package/src/pg-object-storage.ts +109 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AiUsageExecutionSummary, type AiUsageStorage, type RecordAiModelCallInput, type RecordAiModelCallResult, type SummarizeAiUsageExecutionInput, type SummarizeAiUsageExecutionsInput } from "@sixb/core/storage";
|
|
1
|
+
import { type AiModelCallUsageRecord, type AiUsageExecutionSummary, type AiUsageStorage, type GetLatestAiModelCallForExecutionInput, type RecordAiModelCallInput, type RecordAiModelCallResult, type SummarizeAiUsageExecutionInput, type SummarizeAiUsageExecutionsInput } from "@sixb/core/storage";
|
|
2
2
|
import { type PgStoreClient } from "./transactions";
|
|
3
3
|
/** PostgreSQL-backed append-only model-call usage ledger. */
|
|
4
4
|
export declare class PgAiUsageStorage implements AiUsageStorage {
|
|
@@ -6,6 +6,7 @@ export declare class PgAiUsageStorage implements AiUsageStorage {
|
|
|
6
6
|
constructor(sql: PgStoreClient);
|
|
7
7
|
recordModelCall(input: RecordAiModelCallInput): Promise<RecordAiModelCallResult>;
|
|
8
8
|
summarizeExecution(input: SummarizeAiUsageExecutionInput): Promise<AiUsageExecutionSummary>;
|
|
9
|
+
getLatestForExecution(input: GetLatestAiModelCallForExecutionInput): Promise<AiModelCallUsageRecord | null>;
|
|
9
10
|
summarizeExecutions(input: SummarizeAiUsageExecutionsInput): Promise<readonly AiUsageExecutionSummary[]>;
|
|
10
11
|
private findByIdempotencyKey;
|
|
11
12
|
private requireById;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-ai-usage-storage.d.ts","sourceRoot":"","sources":["../src/pg-ai-usage-storage.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"pg-ai-usage-storage.d.ts","sourceRoot":"","sources":["../src/pg-ai-usage-storage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EAInB,KAAK,qCAAqC,EAE1C,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,8BAA8B,EACnC,KAAK,+BAA+B,EACrC,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,KAAK,aAAa,EAAoB,MAAM,gBAAgB,CAAA;AAErE,6DAA6D;AAC7D,qBAAa,gBAAiB,YAAW,cAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,aAAa;IAEzC,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAgFhF,kBAAkB,CACtB,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,uBAAuB,CAAC;IAQ7B,qBAAqB,CACzB,KAAK,EAAE,qCAAqC,GAC3C,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAYnC,mBAAmB,CACvB,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,SAAS,uBAAuB,EAAE,CAAC;YAShC,oBAAoB;YAkBpB,WAAW;YAYX,WAAW;IA6BzB,OAAO,CAAC,aAAa;YAiBP,qBAAqB;CAgBpC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CountObjectsInput, CountObjectsResult, ExistsObjectsInput, ExistsObjectsResult, FacetObjectsInput, FacetObjectsResult, LinkDirection, ObjectLinkRow, ObjectQueryCapabilities, ObjectRow, ObjectStorage, QueryObjectsInput, QueryObjectsResult } from "@sixb/core/storage";
|
|
1
|
+
import type { CountObjectsInput, CountObjectsResult, ExistsObjectsInput, ExistsObjectsResult, FacetObjectsInput, FacetObjectsResult, LinkBatchKey, LinkDirection, ObjectBatchKey, ObjectLinkRow, ObjectQueryCapabilities, ObjectRow, ObjectStorage, QueryObjectLinksInput, QueryObjectLinksResult, QueryObjectsInput, QueryObjectsResult } from "@sixb/core/storage";
|
|
2
2
|
import type { PgStoreClient } from "./transactions";
|
|
3
3
|
/**
|
|
4
4
|
* PostgreSQL-based ObjectStorage implementation.
|
|
@@ -43,7 +43,7 @@ export declare class PgObjectStorage implements ObjectStorage {
|
|
|
43
43
|
objectTypeId: string;
|
|
44
44
|
primaryId: string;
|
|
45
45
|
}[];
|
|
46
|
-
}): Promise<Map<
|
|
46
|
+
}): Promise<Map<ObjectBatchKey, ObjectRow>>;
|
|
47
47
|
listLinksBatch(params: {
|
|
48
48
|
projectId: string;
|
|
49
49
|
items: readonly {
|
|
@@ -51,7 +51,8 @@ export declare class PgObjectStorage implements ObjectStorage {
|
|
|
51
51
|
objectId: string;
|
|
52
52
|
linkId: string;
|
|
53
53
|
}[];
|
|
54
|
-
}): Promise<Map<
|
|
54
|
+
}): Promise<Map<LinkBatchKey, ObjectLinkRow[]>>;
|
|
55
|
+
queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult>;
|
|
55
56
|
listIncidentLinksBatch(params: {
|
|
56
57
|
projectId: string;
|
|
57
58
|
items: readonly {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-object-storage.d.ts","sourceRoot":"","sources":["../src/pg-object-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EAGnB,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,SAAS,EAET,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"pg-object-storage.d.ts","sourceRoot":"","sources":["../src/pg-object-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EAGnB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,SAAS,EAET,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,oBAAoB,CAAA;AAU3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AA4GnD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAgB,YAAW,aAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,aAAa;IAE/C,iBAAiB,IAAI,uBAAuB;IAItC,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA8BpE,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IASpE,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAMvE,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmBpE,cAAc,CAAC,MAAM,EAAE;QAC3B,SAAS,EAAE,MAAM,CAAA;QACjB,YAAY,EAAE,MAAM,CAAA;QACpB,SAAS,EAAE,MAAM,CAAA;KAClB,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAWvB,SAAS,CAAC,MAAM,EAAE;QACtB,SAAS,EAAE,MAAM,CAAA;QACjB,YAAY,EAAE,MAAM,CAAA;QACpB,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,SAAS,CAAC,EAAE,aAAa,CAAA;KAC1B,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;IAmB/B,mBAAmB,CAAC,MAAM,EAAE;QAChC,SAAS,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,SAAS;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAC9D,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAkBrC,cAAc,CAAC,MAAM,EAAE;QAC3B,SAAS,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,SAAS;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAC7E,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,CAAC,CAAC;IAqBzC,UAAU,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IA6E1E,sBAAsB,CAAC,MAAM,EAAE;QACnC,SAAS,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,SAAS;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAC7D,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;IAkC/B,mBAAmB,CAAC,MAAM,EAAE;QAChC,SAAS,EAAE,MAAM,CAAA;QACjB,YAAY,EAAE,MAAM,CAAA;QACpB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,KAAK,EAAE,MAAM,CAAA;KACd,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAmBhE,IAAI,CAAC,MAAM,EAAE;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;QACzC,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,YAAY,CAAC,EAAE,IAAI,CAAA;QACnB,aAAa,CAAC,EAAE,IAAI,CAAA;QACpB,YAAY,CAAC,EAAE,IAAI,CAAA;QACnB,aAAa,CAAC,EAAE,IAAI,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,WAAW,CAAA;QACjD,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KACvB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CA6EhF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixb/pg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "PostgreSQL storage provider for Sixb",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sixb",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"postgres": "^3.4.5"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@sixb/core": "0.1.
|
|
43
|
+
"@sixb/core": "0.1.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@sixb/core": "0.1.
|
|
46
|
+
"@sixb/core": "0.1.4",
|
|
47
47
|
"@types/bun": "latest",
|
|
48
48
|
"typescript": "^5.7.0"
|
|
49
49
|
},
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
rowToThreadRecord,
|
|
25
25
|
} from "./rows"
|
|
26
26
|
|
|
27
|
+
const PG_CHECKPOINT_RUN_ID_BATCH_SIZE = 10_000
|
|
28
|
+
|
|
27
29
|
export class PgAgentContextCheckpointStore implements AgentContextCheckpointStore {
|
|
28
30
|
constructor(private readonly sql: PgStoreClient) {}
|
|
29
31
|
|
|
@@ -172,4 +174,31 @@ export class PgAgentContextCheckpointStore implements AgentContextCheckpointStor
|
|
|
172
174
|
`
|
|
173
175
|
return row ? rowToContextCheckpointRecord(row) : null
|
|
174
176
|
}
|
|
177
|
+
|
|
178
|
+
async getByRunIds(input: {
|
|
179
|
+
readonly projectId: string
|
|
180
|
+
readonly runIds: readonly string[]
|
|
181
|
+
}): Promise<readonly AgentContextCheckpointRecord[]> {
|
|
182
|
+
const runIds = [...new Set(input.runIds)]
|
|
183
|
+
if (runIds.length === 0) return []
|
|
184
|
+
|
|
185
|
+
const rows: AgentContextCheckpointRow[] = []
|
|
186
|
+
for (let offset = 0; offset < runIds.length; offset += PG_CHECKPOINT_RUN_ID_BATCH_SIZE) {
|
|
187
|
+
const batch = runIds.slice(offset, offset + PG_CHECKPOINT_RUN_ID_BATCH_SIZE)
|
|
188
|
+
const placeholders = batch.map((_, index) => `$${index + 2}`).join(", ")
|
|
189
|
+
rows.push(
|
|
190
|
+
...(await this.sql.unsafe<AgentContextCheckpointRow[]>(
|
|
191
|
+
`SELECT * FROM agent_context_checkpoints WHERE project_id = $1 AND created_by_run_id IN (${placeholders})`,
|
|
192
|
+
[input.projectId, ...batch]
|
|
193
|
+
))
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
const byRunId = new Map(
|
|
197
|
+
rows.map((row) => [row.created_by_run_id, rowToContextCheckpointRecord(row)] as const)
|
|
198
|
+
)
|
|
199
|
+
return runIds.flatMap((runId) => {
|
|
200
|
+
const checkpoint = byRunId.get(runId)
|
|
201
|
+
return checkpoint ? [checkpoint] : []
|
|
202
|
+
})
|
|
203
|
+
}
|
|
175
204
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
AiUsageStorageError,
|
|
8
8
|
aggregateAiModelCallUsage,
|
|
9
9
|
assertAiUsageExecutionId,
|
|
10
|
+
type GetLatestAiModelCallForExecutionInput,
|
|
10
11
|
normalizeAiModelCallRecord,
|
|
11
12
|
type RecordAiModelCallInput,
|
|
12
13
|
type RecordAiModelCallResult,
|
|
@@ -110,6 +111,20 @@ export class PgAiUsageStorage implements AiUsageStorage {
|
|
|
110
111
|
return summary!
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
async getLatestForExecution(
|
|
115
|
+
input: GetLatestAiModelCallForExecutionInput
|
|
116
|
+
): Promise<AiModelCallUsageRecord | null> {
|
|
117
|
+
assertNonBlankProjectId(input.projectId)
|
|
118
|
+
assertAiUsageExecutionId(input.executionId)
|
|
119
|
+
const [row] = await this.sql<AiUsageRow[]>`
|
|
120
|
+
SELECT * FROM ai_model_call_usage
|
|
121
|
+
WHERE project_id = ${input.projectId} AND execution_id = ${input.executionId}
|
|
122
|
+
ORDER BY occurred_at DESC, id DESC
|
|
123
|
+
LIMIT 1
|
|
124
|
+
`
|
|
125
|
+
return row ? this.rowToRecord(this.sql, row) : null
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
async summarizeExecutions(
|
|
114
129
|
input: SummarizeAiUsageExecutionsInput
|
|
115
130
|
): Promise<readonly AiUsageExecutionSummary[]> {
|
|
@@ -177,6 +177,8 @@ function compileObjectQueryInternal(
|
|
|
177
177
|
switch (query.kind) {
|
|
178
178
|
case "start":
|
|
179
179
|
return compileStart(projectId, query)
|
|
180
|
+
case "refs":
|
|
181
|
+
return compileRefs(projectId, query)
|
|
180
182
|
case "filter":
|
|
181
183
|
return compileFilter(projectId, query.input, query.predicate)
|
|
182
184
|
case "sort":
|
|
@@ -243,6 +245,54 @@ function compileStart(
|
|
|
243
245
|
}
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
function compileRefs(
|
|
249
|
+
projectId: string,
|
|
250
|
+
query: Extract<ObjectQuery, { kind: "refs" }>
|
|
251
|
+
): CompiledPgObjectQuery {
|
|
252
|
+
if (query.refs.length === 0) {
|
|
253
|
+
throw new Error("[SixbPg] PostgreSQL object storage requires at least one ref")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const order = compileOrder(identityOrderFields())
|
|
257
|
+
const selectedOrder = compileOrder(identityOrderFields(), "selected")
|
|
258
|
+
const refsJson = JSON.stringify(query.refs)
|
|
259
|
+
const requested = `
|
|
260
|
+
SELECT DISTINCT
|
|
261
|
+
ref.value ->> 'objectTypeId' AS object_type_id,
|
|
262
|
+
ref.value ->> 'primaryId' AS primary_id
|
|
263
|
+
FROM jsonb_array_elements(?::text::jsonb) AS ref(value)
|
|
264
|
+
`
|
|
265
|
+
const sql = `
|
|
266
|
+
WITH requested AS (${requested})
|
|
267
|
+
SELECT selected.*, selected.properties AS _cursor_properties
|
|
268
|
+
FROM requested
|
|
269
|
+
JOIN objects AS selected
|
|
270
|
+
ON selected.project_id = ?
|
|
271
|
+
AND selected.object_type_id = requested.object_type_id
|
|
272
|
+
AND selected.primary_id = requested.primary_id
|
|
273
|
+
ORDER BY ${selectedOrder.sql}
|
|
274
|
+
`
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
sql,
|
|
278
|
+
args: [refsJson, projectId, ...selectedOrder.args],
|
|
279
|
+
totalSql: `
|
|
280
|
+
WITH requested AS (${requested})
|
|
281
|
+
SELECT COUNT(*)::bigint AS total
|
|
282
|
+
FROM requested
|
|
283
|
+
JOIN objects AS selected
|
|
284
|
+
ON selected.project_id = ?
|
|
285
|
+
AND selected.object_type_id = requested.object_type_id
|
|
286
|
+
AND selected.primary_id = requested.primary_id
|
|
287
|
+
`,
|
|
288
|
+
totalArgs: [refsJson, projectId],
|
|
289
|
+
order,
|
|
290
|
+
hasMore: () => false,
|
|
291
|
+
trimRows: identityRows,
|
|
292
|
+
nextPageToken: () => undefined,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
246
296
|
function compileFilter(
|
|
247
297
|
projectId: string,
|
|
248
298
|
inputQuery: ObjectQuery,
|
|
@@ -753,6 +803,8 @@ function compileAggregateSource(projectId: string, query: ObjectQuery): Compiled
|
|
|
753
803
|
switch (query.kind) {
|
|
754
804
|
case "start":
|
|
755
805
|
return compileAggregateStart(projectId, query)
|
|
806
|
+
case "refs":
|
|
807
|
+
return compileAggregateRefs(projectId, query)
|
|
756
808
|
case "filter":
|
|
757
809
|
return compileAggregateWhere(projectId, query.input, compilePredicate(query.predicate))
|
|
758
810
|
case "text": {
|
|
@@ -781,7 +833,9 @@ function compileAggregateSource(projectId: string, query: ObjectQuery): Compiled
|
|
|
781
833
|
case "page":
|
|
782
834
|
return compileRowQueryAggregateSource(projectId, query)
|
|
783
835
|
case "vector":
|
|
784
|
-
throw new Error(
|
|
836
|
+
throw new Error(
|
|
837
|
+
`[SixbPg] PostgreSQL object storage does not support query node '${query.kind}'`
|
|
838
|
+
)
|
|
785
839
|
}
|
|
786
840
|
}
|
|
787
841
|
|
|
@@ -803,6 +857,32 @@ function compileAggregateStart(
|
|
|
803
857
|
}
|
|
804
858
|
}
|
|
805
859
|
|
|
860
|
+
function compileAggregateRefs(
|
|
861
|
+
projectId: string,
|
|
862
|
+
query: Extract<ObjectQuery, { kind: "refs" }>
|
|
863
|
+
): CompiledAggregateSource {
|
|
864
|
+
if (query.refs.length === 0) {
|
|
865
|
+
throw new Error("[SixbPg] PostgreSQL object storage requires at least one ref")
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return {
|
|
869
|
+
sql: `
|
|
870
|
+
SELECT selected.project_id, selected.object_type_id, selected.primary_id, selected.properties
|
|
871
|
+
FROM (
|
|
872
|
+
SELECT DISTINCT
|
|
873
|
+
ref.value ->> 'objectTypeId' AS object_type_id,
|
|
874
|
+
ref.value ->> 'primaryId' AS primary_id
|
|
875
|
+
FROM jsonb_array_elements(?::text::jsonb) AS ref(value)
|
|
876
|
+
) AS requested
|
|
877
|
+
JOIN objects AS selected
|
|
878
|
+
ON selected.project_id = ?
|
|
879
|
+
AND selected.object_type_id = requested.object_type_id
|
|
880
|
+
AND selected.primary_id = requested.primary_id
|
|
881
|
+
`,
|
|
882
|
+
args: [JSON.stringify(query.refs), projectId],
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
806
886
|
function compileAggregateWhere(
|
|
807
887
|
projectId: string,
|
|
808
888
|
inputQuery: ObjectQuery,
|
package/src/pg-object-storage.ts
CHANGED
|
@@ -8,15 +8,20 @@ import type {
|
|
|
8
8
|
ExpandedObjectRow,
|
|
9
9
|
FacetObjectsInput,
|
|
10
10
|
FacetObjectsResult,
|
|
11
|
+
LinkBatchKey,
|
|
11
12
|
LinkDirection,
|
|
13
|
+
ObjectBatchKey,
|
|
12
14
|
ObjectLinkRow,
|
|
13
15
|
ObjectQueryCapabilities,
|
|
14
16
|
ObjectRow,
|
|
15
17
|
ObjectRowLinks,
|
|
16
18
|
ObjectStorage,
|
|
19
|
+
QueryObjectLinksInput,
|
|
20
|
+
QueryObjectLinksResult,
|
|
17
21
|
QueryObjectsInput,
|
|
18
22
|
QueryObjectsResult,
|
|
19
23
|
} from "@sixb/core/storage"
|
|
24
|
+
import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
|
|
20
25
|
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
21
26
|
import {
|
|
22
27
|
type CompiledPgObjectQuery,
|
|
@@ -34,6 +39,7 @@ const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
|
34
39
|
facetObjects: true,
|
|
35
40
|
nodes: {
|
|
36
41
|
start: true,
|
|
42
|
+
refs: true,
|
|
37
43
|
filter: true,
|
|
38
44
|
text: true,
|
|
39
45
|
sort: true,
|
|
@@ -85,7 +91,7 @@ const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
|
85
91
|
stablePageTokens: true,
|
|
86
92
|
},
|
|
87
93
|
notes: [
|
|
88
|
-
"PostgreSQL object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSONB properties and object links.",
|
|
94
|
+
"PostgreSQL object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSONB properties and object links.",
|
|
89
95
|
"expand hydrates linked objects in-database (top-N per parent via LATERAL + jsonb_agg); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
|
|
90
96
|
"Exact decimal predicates, ordering, and keyset pagination use PostgreSQL numeric casts.",
|
|
91
97
|
"Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
|
|
@@ -263,8 +269,8 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
263
269
|
async getByPrimaryIdBatch(params: {
|
|
264
270
|
projectId: string
|
|
265
271
|
items: readonly { objectTypeId: string; primaryId: string }[]
|
|
266
|
-
}): Promise<Map<
|
|
267
|
-
const result = new Map<
|
|
272
|
+
}): Promise<Map<ObjectBatchKey, ObjectRow>> {
|
|
273
|
+
const result = new Map<ObjectBatchKey, ObjectRow>()
|
|
268
274
|
if (params.items.length === 0) return result
|
|
269
275
|
const rows = await valuesJoin<ObjectDatabaseRow>(
|
|
270
276
|
this.sql,
|
|
@@ -276,7 +282,7 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
276
282
|
)
|
|
277
283
|
|
|
278
284
|
for (const row of rows) {
|
|
279
|
-
result.set(
|
|
285
|
+
result.set(objectBatchKey(row.object_type_id, row.primary_id), rowToObject(row))
|
|
280
286
|
}
|
|
281
287
|
return result
|
|
282
288
|
}
|
|
@@ -284,8 +290,8 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
284
290
|
async listLinksBatch(params: {
|
|
285
291
|
projectId: string
|
|
286
292
|
items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
|
|
287
|
-
}): Promise<Map<
|
|
288
|
-
const result = new Map<
|
|
293
|
+
}): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
|
|
294
|
+
const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
|
|
289
295
|
if (params.items.length === 0) return result
|
|
290
296
|
const rows = await valuesJoin<LinkDatabaseRow>(
|
|
291
297
|
this.sql,
|
|
@@ -297,7 +303,7 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
297
303
|
)
|
|
298
304
|
|
|
299
305
|
for (const row of rows) {
|
|
300
|
-
const key =
|
|
306
|
+
const key = linkBatchKey(row.source_type_id, row.source_id, row.link_id)
|
|
301
307
|
const existing = result.get(key) ?? []
|
|
302
308
|
existing.push(rowToLink(row))
|
|
303
309
|
result.set(key, existing)
|
|
@@ -305,6 +311,83 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
305
311
|
return result
|
|
306
312
|
}
|
|
307
313
|
|
|
314
|
+
async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
|
|
315
|
+
assertLinkQueryLimit(params.limit)
|
|
316
|
+
if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
|
|
317
|
+
return { links: [], hasMore: false }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const args: SqlParameter[] = [JSON.stringify(params.objectRefs), params.projectId]
|
|
321
|
+
const addArg = (value: SqlParameter): string => {
|
|
322
|
+
args.push(value)
|
|
323
|
+
return `$${args.length}`
|
|
324
|
+
}
|
|
325
|
+
const sourceJoin = `
|
|
326
|
+
SELECT link.*
|
|
327
|
+
FROM links AS link
|
|
328
|
+
JOIN requested
|
|
329
|
+
ON requested.object_type_id = link.source_type_id
|
|
330
|
+
AND requested.object_id = link.source_id
|
|
331
|
+
WHERE link.project_id = $2
|
|
332
|
+
`
|
|
333
|
+
const targetJoin = `
|
|
334
|
+
SELECT link.*
|
|
335
|
+
FROM links AS link
|
|
336
|
+
JOIN requested
|
|
337
|
+
ON requested.object_type_id = link.target_type_id
|
|
338
|
+
AND requested.object_id = link.target_id
|
|
339
|
+
WHERE link.project_id = $2
|
|
340
|
+
`
|
|
341
|
+
const incidentSql =
|
|
342
|
+
params.direction === "outgoing"
|
|
343
|
+
? sourceJoin
|
|
344
|
+
: params.direction === "incoming"
|
|
345
|
+
? targetJoin
|
|
346
|
+
: `${sourceJoin} UNION ${targetJoin}`
|
|
347
|
+
|
|
348
|
+
const predicates: string[] = []
|
|
349
|
+
if (params.linkId !== undefined) {
|
|
350
|
+
predicates.push(`link_id = ${addArg(params.linkId)}::text`)
|
|
351
|
+
}
|
|
352
|
+
if (params.endpointObjectTypeIds !== undefined) {
|
|
353
|
+
const allowedTypes = addArg(JSON.stringify([...new Set(params.endpointObjectTypeIds)]))
|
|
354
|
+
predicates.push(
|
|
355
|
+
`source_type_id IN (SELECT jsonb_array_elements_text(${allowedTypes}::text::jsonb))`,
|
|
356
|
+
`target_type_id IN (SELECT jsonb_array_elements_text(${allowedTypes}::text::jsonb))`
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
if (params.after) {
|
|
360
|
+
const cursor = params.after.map((value) => `${addArg(value)}::text`)
|
|
361
|
+
predicates.push(
|
|
362
|
+
`(source_type_id, source_id, link_id, target_type_id, target_id) > (${cursor.join(", ")})`
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
const limit = addArg(params.limit + 1)
|
|
366
|
+
const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : ""
|
|
367
|
+
const rows = await this.sql.unsafe<LinkDatabaseRow[]>(
|
|
368
|
+
`
|
|
369
|
+
WITH requested AS (
|
|
370
|
+
SELECT DISTINCT
|
|
371
|
+
requested."objectTypeId" AS object_type_id,
|
|
372
|
+
requested."primaryId" AS object_id
|
|
373
|
+
FROM jsonb_to_recordset($1::text::jsonb)
|
|
374
|
+
AS requested("objectTypeId" text, "primaryId" text)
|
|
375
|
+
), incident AS (${incidentSql})
|
|
376
|
+
SELECT *
|
|
377
|
+
FROM incident
|
|
378
|
+
${whereSql}
|
|
379
|
+
ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
|
|
380
|
+
LIMIT ${limit}
|
|
381
|
+
`,
|
|
382
|
+
args
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
links: rows.slice(0, params.limit).map((row) => rowToLink(row)),
|
|
387
|
+
hasMore: rows.length > params.limit,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
308
391
|
async listIncidentLinksBatch(params: {
|
|
309
392
|
projectId: string
|
|
310
393
|
items: readonly { objectTypeId: string; objectId: string }[]
|
|
@@ -314,8 +397,8 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
314
397
|
|
|
315
398
|
// Cover both link directions with two index-friendly equality joins (source-side, then
|
|
316
399
|
// target-side) rather than a single OR-join, which would defeat index usage. This is a constant
|
|
317
|
-
// number of round trips regardless of
|
|
318
|
-
//
|
|
400
|
+
// number of round trips regardless of the requested object count. They are issued sequentially
|
|
401
|
+
// because callers may run on a serializable transaction's single connection. A link incident to
|
|
319
402
|
// two listed objects matches both halves and is de-duplicated below.
|
|
320
403
|
const sourceRows = await valuesJoin<LinkDatabaseRow>(
|
|
321
404
|
this.sql,
|
|
@@ -337,10 +420,7 @@ export class PgObjectStorage implements ObjectStorage {
|
|
|
337
420
|
const deduped = new Map<string, ObjectLinkRow>()
|
|
338
421
|
for (const row of [...sourceRows, ...targetRows]) {
|
|
339
422
|
const link = rowToLink(row)
|
|
340
|
-
deduped.set(
|
|
341
|
-
`${link.sourceTypeId}:${link.sourceId}:${link.linkId}:${link.targetTypeId}:${link.targetId}`,
|
|
342
|
-
link
|
|
343
|
-
)
|
|
423
|
+
deduped.set(linkIdentity(link), link)
|
|
344
424
|
}
|
|
345
425
|
return [...deduped.values()]
|
|
346
426
|
}
|
|
@@ -601,6 +681,22 @@ function rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
|
|
|
601
681
|
}
|
|
602
682
|
}
|
|
603
683
|
|
|
684
|
+
function linkIdentity(link: ObjectLinkRow): string {
|
|
685
|
+
return JSON.stringify([
|
|
686
|
+
link.sourceTypeId,
|
|
687
|
+
link.sourceId,
|
|
688
|
+
link.linkId,
|
|
689
|
+
link.targetTypeId,
|
|
690
|
+
link.targetId,
|
|
691
|
+
])
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function assertLinkQueryLimit(limit: number): void {
|
|
695
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
696
|
+
throw new Error("Object link query limit must be a positive safe integer.")
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
604
700
|
interface ObjectDatabaseRow {
|
|
605
701
|
project_id: string
|
|
606
702
|
object_type_id: string
|