@sixb/sqlite 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 { 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 SqliteStoreConnection } from "./transactions";
3
3
  export interface SqliteObjectStorageOptions {
4
4
  /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
@@ -39,7 +39,7 @@ export declare class SqliteObjectStorage implements ObjectStorage {
39
39
  objectTypeId: string;
40
40
  primaryId: string;
41
41
  }[];
42
- }): Promise<Map<string, ObjectRow>>;
42
+ }): Promise<Map<ObjectBatchKey, ObjectRow>>;
43
43
  listLinksBatch(params: {
44
44
  projectId: string;
45
45
  items: readonly {
@@ -47,7 +47,8 @@ export declare class SqliteObjectStorage implements ObjectStorage {
47
47
  objectId: string;
48
48
  linkId: string;
49
49
  }[];
50
- }): Promise<Map<string, ObjectLinkRow[]>>;
50
+ }): Promise<Map<LinkBatchKey, ObjectLinkRow[]>>;
51
+ queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult>;
51
52
  listIncidentLinksBatch(params: {
52
53
  projectId: string;
53
54
  items: readonly {
@@ -1 +1 @@
1
- {"version":3,"file":"object-storage.d.ts","sourceRoot":"","sources":["../src/object-storage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EAGnB,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EAEb,aAAa,EACb,uBAAuB,EACvB,SAAS,EAET,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,gBAAgB,CAAA;AAEvB,MAAM,WAAW,0BAA0B;IACzC,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gEAAgE;IAChE,UAAU,CAAC,EAAE,qBAAqB,CAAA;CACnC;AAmED;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAU;gBAEjB,OAAO,GAAE,0BAA+B;IASpD,iBAAiB,IAAI,uBAAuB;IAItC,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAsBpE,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IASpE,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAKvE,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAUpE,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;IAQvB,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;IA8B/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,MAAM,EAAE,SAAS,CAAC,CAAC;IAoB7B,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,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;IAwBnC,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;IAmB/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;IA4BhE,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;IAuE/E,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAejB;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"object-storage.d.ts","sourceRoot":"","sources":["../src/object-storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/sqlite",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "SQLite storage provider for Sixb",
5
5
  "keywords": [
6
6
  "sixb",
@@ -36,10 +36,10 @@
36
36
  "prepack": "bun run build"
37
37
  },
38
38
  "peerDependencies": {
39
- "@sixb/core": "0.1.3"
39
+ "@sixb/core": "0.1.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@sixb/core": "0.1.3",
42
+ "@sixb/core": "0.1.4",
43
43
  "@types/bun": "latest",
44
44
  "typescript": "^5.7.0"
45
45
  },
@@ -23,6 +23,8 @@ import {
23
23
  rowToThreadRecord,
24
24
  } from "./rows"
25
25
 
26
+ const SQLITE_CHECKPOINT_RUN_ID_BATCH_SIZE = 500
27
+
26
28
  export class SqliteAgentContextCheckpointStore implements AgentContextCheckpointStore {
27
29
  constructor(private readonly db: Database) {}
28
30
 
@@ -146,6 +148,34 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
146
148
  return this.findLatest(input.projectId, input.threadId)
147
149
  }
148
150
 
151
+ async getByRunIds(input: {
152
+ readonly projectId: string
153
+ readonly runIds: readonly string[]
154
+ }): Promise<readonly AgentContextCheckpointRecord[]> {
155
+ const runIds = [...new Set(input.runIds)]
156
+ if (runIds.length === 0) return []
157
+
158
+ const rows: AgentContextCheckpointRow[] = []
159
+ for (let offset = 0; offset < runIds.length; offset += SQLITE_CHECKPOINT_RUN_ID_BATCH_SIZE) {
160
+ const batch = runIds.slice(offset, offset + SQLITE_CHECKPOINT_RUN_ID_BATCH_SIZE)
161
+ const placeholders = batch.map(() => "?").join(", ")
162
+ rows.push(
163
+ ...(this.db
164
+ .query(
165
+ `SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id IN (${placeholders})`
166
+ )
167
+ .all(input.projectId, ...batch) as AgentContextCheckpointRow[])
168
+ )
169
+ }
170
+ const byRunId = new Map(
171
+ rows.map((row) => [row.created_by_run_id, rowToContextCheckpointRecord(row)] as const)
172
+ )
173
+ return runIds.flatMap((runId) => {
174
+ const checkpoint = byRunId.get(runId)
175
+ return checkpoint ? [checkpoint] : []
176
+ })
177
+ }
178
+
149
179
  private findLatest(projectId: string, threadId: string): AgentContextCheckpointRecord | null {
150
180
  const row = this.db
151
181
  .query(
@@ -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,
@@ -84,6 +85,24 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
84
85
  return summary!
85
86
  }
86
87
 
88
+ async getLatestForExecution(
89
+ input: GetLatestAiModelCallForExecutionInput
90
+ ): Promise<AiModelCallUsageRecord | null> {
91
+ assertNonBlankProjectId(input.projectId)
92
+ assertAiUsageExecutionId(input.executionId)
93
+ const row = this.connection.db
94
+ .query(
95
+ `
96
+ SELECT * FROM ai_model_call_usage
97
+ WHERE project_id = ? AND execution_id = ?
98
+ ORDER BY occurred_at DESC, id DESC
99
+ LIMIT 1
100
+ `
101
+ )
102
+ .get(input.projectId, input.executionId) as AiUsageRow | null
103
+ return row ? this.rowToRecord(row) : null
104
+ }
105
+
87
106
  async summarizeExecutions(
88
107
  input: SummarizeAiUsageExecutionsInput
89
108
  ): Promise<readonly AiUsageExecutionSummary[]> {
@@ -98,6 +98,8 @@ function compileObjectQueryInternal(
98
98
  switch (query.kind) {
99
99
  case "start":
100
100
  return compileStart(projectId, query)
101
+ case "refs":
102
+ return compileRefs(projectId, query)
101
103
  case "filter":
102
104
  return compileFilter(projectId, query.input, query.predicate)
103
105
  case "sort":
@@ -162,6 +164,54 @@ function compileStart(
162
164
  }
163
165
  }
164
166
 
167
+ function compileRefs(
168
+ projectId: string,
169
+ query: Extract<ObjectQuery, { kind: "refs" }>
170
+ ): CompiledObjectQuery {
171
+ if (query.refs.length === 0) {
172
+ throw new Error("[Sixb] SQLite object storage requires at least one ref")
173
+ }
174
+
175
+ const order = compileOrder(identityOrderFields())
176
+ const selectedOrder = compileOrder(identityOrderFields(), "selected")
177
+ const refsJson = JSON.stringify(query.refs)
178
+ const requested = `
179
+ SELECT DISTINCT
180
+ json_extract(ref.value, '$.objectTypeId') AS object_type_id,
181
+ json_extract(ref.value, '$.primaryId') AS primary_id
182
+ FROM json_each(?) AS ref
183
+ `
184
+ const sql = `
185
+ WITH requested AS (${requested})
186
+ SELECT selected.*, selected.properties AS _cursor_properties
187
+ FROM requested
188
+ JOIN objects AS selected
189
+ ON selected.project_id = ?
190
+ AND selected.object_type_id = requested.object_type_id
191
+ AND selected.primary_id = requested.primary_id
192
+ ORDER BY ${selectedOrder.sql}
193
+ `
194
+
195
+ return {
196
+ sql,
197
+ args: [refsJson, projectId, ...selectedOrder.args],
198
+ totalSql: `
199
+ WITH requested AS (${requested})
200
+ SELECT COUNT(*) AS total
201
+ FROM requested
202
+ JOIN objects AS selected
203
+ ON selected.project_id = ?
204
+ AND selected.object_type_id = requested.object_type_id
205
+ AND selected.primary_id = requested.primary_id
206
+ `,
207
+ totalArgs: [refsJson, projectId],
208
+ order,
209
+ hasMore: () => false,
210
+ trimRows: identityRows,
211
+ nextPageToken: () => undefined,
212
+ }
213
+ }
214
+
165
215
  function compileFilter(
166
216
  projectId: string,
167
217
  inputQuery: ObjectQuery,
@@ -9,16 +9,21 @@ import type {
9
9
  ExpandedObjectRow,
10
10
  FacetObjectsInput,
11
11
  FacetObjectsResult,
12
+ LinkBatchKey,
12
13
  LinkDirection,
14
+ ObjectBatchKey,
13
15
  ObjectFacetRequest,
14
16
  ObjectLinkRow,
15
17
  ObjectQueryCapabilities,
16
18
  ObjectRow,
17
19
  ObjectRowLinks,
18
20
  ObjectStorage,
21
+ QueryObjectLinksInput,
22
+ QueryObjectLinksResult,
19
23
  QueryObjectsInput,
20
24
  QueryObjectsResult,
21
25
  } from "@sixb/core/storage"
26
+ import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
22
27
  import { installFreshSqliteSchema } from "./migrations"
23
28
  import { type CompiledObjectQuery, compileObjectQuery } from "./object-query-compiler"
24
29
  import {
@@ -41,6 +46,7 @@ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
41
46
  facetObjects: true,
42
47
  nodes: {
43
48
  start: true,
49
+ refs: true,
44
50
  filter: true,
45
51
  text: true,
46
52
  sort: true,
@@ -92,7 +98,7 @@ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
92
98
  stablePageTokens: true,
93
99
  },
94
100
  notes: [
95
- "SQLite object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
101
+ "SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
96
102
  "expand hydrates linked objects in-database (top-N per parent via row_number() + json_group_array); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
97
103
  "Ordered decimal predicates and sorting use the bounded core fallback because SQLite has no native exact decimal type; canonical decimal equality remains pushdown-safe.",
98
104
  "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
@@ -219,22 +225,24 @@ export class SqliteObjectStorage implements ObjectStorage {
219
225
  async getByPrimaryIdBatch(params: {
220
226
  projectId: string
221
227
  items: readonly { objectTypeId: string; primaryId: string }[]
222
- }): Promise<Map<string, ObjectRow>> {
223
- const result = new Map<string, ObjectRow>()
228
+ }): Promise<Map<ObjectBatchKey, ObjectRow>> {
229
+ const result = new Map<ObjectBatchKey, ObjectRow>()
224
230
  if (params.items.length === 0) return result
225
231
 
226
- const stmt = this.db.query(
227
- "SELECT * FROM objects WHERE project_id = ? AND object_type_id = ? AND primary_id = ?"
228
- )
229
- for (const item of params.items) {
230
- const row = stmt.get(
231
- params.projectId,
232
- item.objectTypeId,
233
- item.primaryId
234
- ) as DatabaseRow | null
235
- if (row) {
236
- result.set(`${item.objectTypeId}:${item.primaryId}`, this.rowToObject(row))
237
- }
232
+ const rows = this.db
233
+ .query(
234
+ `
235
+ SELECT object.*
236
+ FROM json_each(?) AS requested
237
+ JOIN objects AS object
238
+ ON object.project_id = ?
239
+ AND object.object_type_id = json_extract(requested.value, '$.objectTypeId')
240
+ AND object.primary_id = json_extract(requested.value, '$.primaryId')
241
+ `
242
+ )
243
+ .all(JSON.stringify(params.items), params.projectId) as DatabaseRow[]
244
+ for (const row of rows) {
245
+ result.set(objectBatchKey(row.object_type_id, row.primary_id), this.rowToObject(row))
238
246
  }
239
247
  return result
240
248
  }
@@ -242,8 +250,8 @@ export class SqliteObjectStorage implements ObjectStorage {
242
250
  async listLinksBatch(params: {
243
251
  projectId: string
244
252
  items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
245
- }): Promise<Map<string, ObjectLinkRow[]>> {
246
- const result = new Map<string, ObjectLinkRow[]>()
253
+ }): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
254
+ const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
247
255
  if (params.items.length === 0) return result
248
256
 
249
257
  const stmt = this.db.query(
@@ -258,7 +266,7 @@ export class SqliteObjectStorage implements ObjectStorage {
258
266
  ) as LinkDatabaseRow[]
259
267
  if (rows.length > 0) {
260
268
  result.set(
261
- `${item.objectTypeId}:${item.objectId}:${item.linkId}`,
269
+ linkBatchKey(item.objectTypeId, item.objectId, item.linkId),
262
270
  rows.map((r) => this.rowToLink(r))
263
271
  )
264
272
  }
@@ -266,26 +274,116 @@ export class SqliteObjectStorage implements ObjectStorage {
266
274
  return result
267
275
  }
268
276
 
277
+ async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
278
+ assertLinkQueryLimit(params.limit)
279
+ if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
280
+ return { links: [], hasMore: false }
281
+ }
282
+
283
+ const sourceJoin = `
284
+ SELECT link.*
285
+ FROM links AS link
286
+ JOIN requested
287
+ ON requested.object_type_id = link.source_type_id
288
+ AND requested.object_id = link.source_id
289
+ WHERE link.project_id = ?
290
+ `
291
+ const targetJoin = `
292
+ SELECT link.*
293
+ FROM links AS link
294
+ JOIN requested
295
+ ON requested.object_type_id = link.target_type_id
296
+ AND requested.object_id = link.target_id
297
+ WHERE link.project_id = ?
298
+ `
299
+ const incidentSql =
300
+ params.direction === "outgoing"
301
+ ? sourceJoin
302
+ : params.direction === "incoming"
303
+ ? targetJoin
304
+ : `${sourceJoin} UNION ${targetJoin}`
305
+ const args: (string | number)[] = [JSON.stringify(params.objectRefs), params.projectId]
306
+ if (params.direction === "both") args.push(params.projectId)
307
+
308
+ const predicates: string[] = []
309
+ if (params.linkId !== undefined) {
310
+ predicates.push("link_id = ?")
311
+ args.push(params.linkId)
312
+ }
313
+ if (params.endpointObjectTypeIds !== undefined) {
314
+ const allowedTypes = JSON.stringify([...new Set(params.endpointObjectTypeIds)])
315
+ predicates.push(
316
+ "source_type_id IN (SELECT value FROM json_each(?))",
317
+ "target_type_id IN (SELECT value FROM json_each(?))"
318
+ )
319
+ args.push(allowedTypes, allowedTypes)
320
+ }
321
+ if (params.after) {
322
+ predicates.push(
323
+ "(source_type_id, source_id, link_id, target_type_id, target_id) > (?, ?, ?, ?, ?)"
324
+ )
325
+ args.push(...params.after)
326
+ }
327
+ args.push(params.limit + 1)
328
+
329
+ const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : ""
330
+ const rows = this.db
331
+ .query(
332
+ `
333
+ WITH requested AS (
334
+ SELECT DISTINCT
335
+ json_extract(value, '$.objectTypeId') AS object_type_id,
336
+ json_extract(value, '$.primaryId') AS object_id
337
+ FROM json_each(?)
338
+ ), incident AS (${incidentSql})
339
+ SELECT *
340
+ FROM incident
341
+ ${whereSql}
342
+ ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
343
+ LIMIT ?
344
+ `
345
+ )
346
+ .all(...args) as LinkDatabaseRow[]
347
+
348
+ return {
349
+ links: rows.slice(0, params.limit).map((row) => this.rowToLink(row)),
350
+ hasMore: rows.length > params.limit,
351
+ }
352
+ }
353
+
269
354
  async listIncidentLinksBatch(params: {
270
355
  projectId: string
271
356
  items: readonly { objectTypeId: string; objectId: string }[]
272
357
  }): Promise<readonly ObjectLinkRow[]> {
273
- const deduped = new Map<string, ObjectLinkRow>()
274
- for (const item of params.items) {
275
- const rows = await this.listLinks({
276
- projectId: params.projectId,
277
- objectTypeId: item.objectTypeId,
278
- objectId: item.objectId,
279
- direction: "both",
280
- })
281
- for (const row of rows) {
282
- deduped.set(
283
- `${row.sourceTypeId}:${row.sourceId}:${row.linkId}:${row.targetTypeId}:${row.targetId}`,
284
- row
285
- )
286
- }
287
- }
288
- return [...deduped.values()]
358
+ if (params.items.length === 0) return []
359
+
360
+ const rows = this.db
361
+ .query(
362
+ `
363
+ WITH requested AS (
364
+ SELECT
365
+ json_extract(value, '$.objectTypeId') AS object_type_id,
366
+ json_extract(value, '$.objectId') AS object_id
367
+ FROM json_each(?)
368
+ )
369
+ SELECT link.*
370
+ FROM links AS link
371
+ JOIN requested
372
+ ON requested.object_type_id = link.source_type_id
373
+ AND requested.object_id = link.source_id
374
+ WHERE link.project_id = ?
375
+ UNION
376
+ SELECT link.*
377
+ FROM links AS link
378
+ JOIN requested
379
+ ON requested.object_type_id = link.target_type_id
380
+ AND requested.object_id = link.target_id
381
+ WHERE link.project_id = ?
382
+ `
383
+ )
384
+ .all(JSON.stringify(params.items), params.projectId, params.projectId) as LinkDatabaseRow[]
385
+
386
+ return rows.map((row) => this.rowToLink(row))
289
387
  }
290
388
 
291
389
  async listByPrimaryIdPage(params: {
@@ -457,6 +555,12 @@ function assertReconciliationPageLimit(limit: number): void {
457
555
  }
458
556
  }
459
557
 
558
+ function assertLinkQueryLimit(limit: number): void {
559
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
560
+ throw new Error("Object link query limit must be a positive safe integer.")
561
+ }
562
+ }
563
+
460
564
  function readTotal(db: Database, compiled: CompiledObjectQuery): number {
461
565
  const row = db.query(compiled.totalSql).get(...compiled.totalArgs) as { total: number }
462
566
  return row.total