@sixb/pg 0.1.3 → 0.1.4

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.
@@ -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,EAGL,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EAKnB,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,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
+ {"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<string, ObjectRow>>;
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<string, ObjectLinkRow[]>>;
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":"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"}
1
+ {"version":3,"file":"pg-object-storage.d.ts","sourceRoot":"","sources":["../src/pg-object-storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/pg",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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.3"
43
+ "@sixb/core": "0.1.4"
44
44
  },
45
45
  "devDependencies": {
46
- "@sixb/core": "0.1.3",
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(`[SixbPg] PostgreSQL object storage does not support query node 'vector'`)
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,
@@ -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<string, ObjectRow>> {
267
- const result = new Map<string, ObjectRow>()
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(`${row.object_type_id}:${row.primary_id}`, rowToObject(row))
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<string, ObjectLinkRow[]>> {
288
- const result = new Map<string, ObjectLinkRow[]>()
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 = `${row.source_type_id}:${row.source_id}:${row.link_id}`
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 how many objects are deleted — issued sequentially because
318
- // these reads run on the serializable commit transaction's single connection. A link incident to
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