@sixb/sqlite 0.1.6 → 0.1.8

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.
Files changed (91) hide show
  1. package/dist/agents/rows.d.ts +7 -4
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/ai-cost-storage.d.ts +2 -1
  9. package/dist/ai-cost-storage.d.ts.map +1 -1
  10. package/dist/ai-limit-storage.d.ts +35 -0
  11. package/dist/ai-limit-storage.d.ts.map +1 -0
  12. package/dist/ai-usage-storage.d.ts +1 -0
  13. package/dist/ai-usage-storage.d.ts.map +1 -1
  14. package/dist/auth-storage/access-tokens.d.ts +1 -1
  15. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  16. package/dist/auth-storage/auth-storage.d.ts +4 -1
  17. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  18. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  19. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  20. package/dist/auth-storage/rows.d.ts +19 -1
  21. package/dist/auth-storage/rows.d.ts.map +1 -1
  22. package/dist/auth-storage/shared.d.ts +1 -1
  23. package/dist/auth-storage/shared.d.ts.map +1 -1
  24. package/dist/execution-storage.d.ts +3 -2
  25. package/dist/execution-storage.d.ts.map +1 -1
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3443 -789
  29. package/dist/index.js.map +29 -20
  30. package/dist/migrations.d.ts.map +1 -1
  31. package/dist/objects/index.d.ts +3 -0
  32. package/dist/objects/index.d.ts.map +1 -0
  33. package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
  34. package/dist/objects/query-compiler.d.ts.map +1 -0
  35. package/dist/objects/read-scope.d.ts +17 -0
  36. package/dist/objects/read-scope.d.ts.map +1 -0
  37. package/dist/objects/reader.d.ts +51 -0
  38. package/dist/objects/reader.d.ts.map +1 -0
  39. package/dist/objects/rows.d.ts +33 -0
  40. package/dist/objects/rows.d.ts.map +1 -0
  41. package/dist/objects/selected-reader.d.ts +9 -0
  42. package/dist/objects/selected-reader.d.ts.map +1 -0
  43. package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
  44. package/dist/objects/storage.d.ts.map +1 -0
  45. package/dist/share-grant-storage.d.ts +20 -0
  46. package/dist/share-grant-storage.d.ts.map +1 -0
  47. package/dist/share-session-storage.d.ts +19 -0
  48. package/dist/share-session-storage.d.ts.map +1 -0
  49. package/dist/storage-errors.d.ts +1 -0
  50. package/dist/storage-errors.d.ts.map +1 -1
  51. package/dist/transactions.d.ts +8 -0
  52. package/dist/transactions.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/agents/rows.ts +36 -9
  55. package/src/agents/runs.ts +152 -13
  56. package/src/agents/threads.ts +2 -17
  57. package/src/ai-cost-groups.ts +100 -0
  58. package/src/ai-cost-storage.ts +67 -25
  59. package/src/ai-limit-storage.ts +981 -0
  60. package/src/ai-usage-storage.ts +24 -1
  61. package/src/auth-storage/access-tokens.ts +77 -68
  62. package/src/auth-storage/auth-storage.ts +80 -1
  63. package/src/auth-storage/device-authorizations.ts +153 -0
  64. package/src/auth-storage/rows.ts +41 -0
  65. package/src/auth-storage/shared.ts +1 -0
  66. package/src/execution-storage.ts +22 -3
  67. package/src/index.ts +24 -3
  68. package/src/migrations/029-model-accounting.sql +79 -0
  69. package/src/migrations/030-ai-usage-limits.sql +100 -0
  70. package/src/migrations/031-subagent-runs.sql +231 -0
  71. package/src/migrations/032-conversation-run-spec.sql +78 -0
  72. package/src/migrations/033-retire-agent-definitions.sql +89 -0
  73. package/src/migrations/034-device-authorizations.sql +22 -0
  74. package/src/migrations/035-share-grants.sql +66 -0
  75. package/src/migrations/036-share-sessions.sql +226 -0
  76. package/src/migrations.ts +22 -0
  77. package/src/objects/index.ts +2 -0
  78. package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
  79. package/src/objects/read-scope.ts +316 -0
  80. package/src/objects/reader.ts +526 -0
  81. package/src/objects/rows.ts +123 -0
  82. package/src/objects/selected-reader.ts +119 -0
  83. package/src/objects/storage.ts +296 -0
  84. package/src/share-grant-storage.ts +252 -0
  85. package/src/share-session-storage.ts +189 -0
  86. package/src/storage-errors.ts +4 -0
  87. package/src/transactions.ts +32 -0
  88. package/src/workflow-run-storage.ts +8 -8
  89. package/dist/object-query-compiler.d.ts.map +0 -1
  90. package/dist/object-storage.d.ts.map +0 -1
  91. package/src/object-storage.ts +0 -709
@@ -0,0 +1,526 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { ObjectQuery } from "@sixb/core"
3
+ import type {
4
+ CountObjectsInput,
5
+ CountObjectsResult,
6
+ ExistsObjectsInput,
7
+ ExistsObjectsResult,
8
+ FacetObjectsInput,
9
+ FacetObjectsResult,
10
+ LinkBatchKey,
11
+ LinkDirection,
12
+ ObjectBatchKey,
13
+ ObjectFacetRequest,
14
+ ObjectLinkRow,
15
+ ObjectReadStorage,
16
+ ObjectRow,
17
+ QueryObjectLinksInput,
18
+ QueryObjectLinksResult,
19
+ QueryObjectsInput,
20
+ QueryObjectsResult,
21
+ } from "@sixb/core/storage"
22
+ import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
23
+ import {
24
+ type CompiledObjectQuery,
25
+ compileObjectQuery,
26
+ type SqliteObjectQuerySource,
27
+ } from "./query-compiler"
28
+ import type { SqliteSelectedObjectReadSource } from "./read-scope"
29
+ import {
30
+ type DatabaseRow,
31
+ type LinkDatabaseRow,
32
+ type ObjectQueryDatabaseRow,
33
+ queryRowToObject,
34
+ rowToLink,
35
+ rowToObject,
36
+ } from "./rows"
37
+
38
+ /** Synchronous SQL reads over one explicit source; transaction and budget ownership stay outside. */
39
+ export class SqliteObjectReader {
40
+ constructor(
41
+ private readonly db: Database,
42
+ private readonly source: SqliteObjectQuerySource | SqliteSelectedObjectReadSource
43
+ ) {}
44
+
45
+ queryObjects(params: QueryObjectsInput): QueryObjectsResult {
46
+ const source = this.source
47
+ const compiled = compileObjectQuery(params.projectId, params.query, {
48
+ includeTotal: params.includeTotal,
49
+ source,
50
+ })
51
+ const total = params.includeTotal === false ? undefined : readTotal(this.db, compiled)
52
+ const rawRows = this.db.query(compiled.sql).all(...compiled.args) as ObjectQueryDatabaseRow[]
53
+ const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
54
+ const hasMore =
55
+ total === undefined && compiled.hasMoreProbe
56
+ ? compiled.hasMoreProbe.hasMore(
57
+ this.db.query(compiled.hasMoreProbe.sql).all(...compiled.hasMoreProbe.args).length
58
+ )
59
+ : compiled.hasMore(rawRows.length, total)
60
+
61
+ return {
62
+ objects: rows.map((row) => queryRowToObject(row)),
63
+ hasMore,
64
+ nextPageToken: compiled.nextPageToken(rows, rawRows.length),
65
+ ...(total === undefined ? {} : { total }),
66
+ }
67
+ }
68
+
69
+ countObjects(params: CountObjectsInput): CountObjectsResult {
70
+ const source = this.source
71
+ return {
72
+ count: readTotal(
73
+ this.db,
74
+ compileObjectQuery(params.projectId, stripOuterRowShape(params.query), {
75
+ source,
76
+ })
77
+ ),
78
+ }
79
+ }
80
+
81
+ existsObjects(params: ExistsObjectsInput): ExistsObjectsResult {
82
+ const source = this.source
83
+ const compiled = compileObjectQuery(params.projectId, existsProbeQuery(params.query), {
84
+ source,
85
+ })
86
+ return { exists: this.db.query(compiled.sql).get(...compiled.args) !== null }
87
+ }
88
+
89
+ facetObjects(params: FacetObjectsInput): FacetObjectsResult {
90
+ const source = this.source
91
+ const compiled = compileObjectQuery(params.projectId, stripOuterRowShape(params.query), {
92
+ source,
93
+ })
94
+ return {
95
+ facets: params.facets.map((facet) => ({
96
+ propertyId: facet.propertyId,
97
+ buckets: readFacetBuckets(this.db, compiled, facet),
98
+ })),
99
+ }
100
+ }
101
+
102
+ getByPrimaryId(params: {
103
+ projectId: string
104
+ objectTypeId: string
105
+ primaryId: string
106
+ }): ObjectRow | null {
107
+ const source = this.source
108
+ const objectsTable = source.objectsTable
109
+ const statement = source.wrapStatement(
110
+ `SELECT * FROM ${objectsTable} WHERE project_id = ? AND object_type_id = ? AND primary_id = ?`,
111
+ [params.projectId, params.objectTypeId, params.primaryId]
112
+ )
113
+ const row = this.db.query(statement.sql).get(...statement.args) as DatabaseRow | null
114
+
115
+ return row ? rowToObject(row) : null
116
+ }
117
+
118
+ selectsObjectProperties(
119
+ params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
120
+ ): readonly boolean[] {
121
+ const source = this.source
122
+ const result = params.items.map(() => false)
123
+ if (params.items.length === 0) return result
124
+
125
+ const items = params.items.map((item, batchIndex) => ({ ...item, batchIndex }))
126
+ const requestedType = "CAST(json_extract(requested.value, '$.objectTypeId') AS TEXT)"
127
+ const requestedId = "CAST(json_extract(requested.value, '$.primaryId') AS TEXT)"
128
+ const requestedProperty = "CAST(json_extract(requested.value, '$.propertyId') AS TEXT)"
129
+ const selected = "objectPropertyPermissionsTable" in source
130
+ const storedTable = selected ? source.objectPropertyPermissionsTable : source.objectsTable
131
+ const propertyJoin = selected ? `AND stored.property_id = ${requestedProperty}` : ""
132
+ const statement = source.wrapStatement(
133
+ `SELECT DISTINCT
134
+ CAST(json_extract(requested.value, '$.batchIndex') AS INTEGER) AS _batch_index
135
+ FROM ${storedTable} AS stored
136
+ JOIN json_each(?) AS requested
137
+ ON stored.object_type_id = ${requestedType}
138
+ AND stored.primary_id = ${requestedId}
139
+ ${propertyJoin}
140
+ WHERE stored.project_id = ?`,
141
+ [JSON.stringify(items), params.projectId]
142
+ )
143
+ const rows = this.db
144
+ .query(statement.sql)
145
+ .all(...statement.args) as PropertyPermissionBatchDatabaseRow[]
146
+ for (const row of rows) result[row._batch_index] = true
147
+ return result
148
+ }
149
+
150
+ listLinks(params: {
151
+ projectId: string
152
+ objectTypeId: string
153
+ objectId: string
154
+ linkId?: string
155
+ direction?: LinkDirection
156
+ }): readonly ObjectLinkRow[] {
157
+ const source = this.source
158
+ const direction = params.direction ?? "outgoing"
159
+ const directionWhere =
160
+ direction === "incoming"
161
+ ? "target_type_id = ? AND target_id = ?"
162
+ : direction === "both"
163
+ ? "((source_type_id = ? AND source_id = ?) OR (target_type_id = ? AND target_id = ?))"
164
+ : "source_type_id = ? AND source_id = ?"
165
+ let query = `SELECT * FROM ${source.linksTable} WHERE project_id = ? AND ${directionWhere}`
166
+ const args: (string | number)[] =
167
+ direction === "both"
168
+ ? [
169
+ params.projectId,
170
+ params.objectTypeId,
171
+ params.objectId,
172
+ params.objectTypeId,
173
+ params.objectId,
174
+ ]
175
+ : [params.projectId, params.objectTypeId, params.objectId]
176
+
177
+ if (params.linkId) {
178
+ query += " AND link_id = ?"
179
+ args.push(params.linkId)
180
+ }
181
+
182
+ const statement = source.wrapStatement(query, args)
183
+ const rows = this.db.query(statement.sql).all(...statement.args) as LinkDatabaseRow[]
184
+
185
+ return rows.map((row) => rowToLink(row))
186
+ }
187
+
188
+ getByPrimaryIdBatch(params: {
189
+ projectId: string
190
+ items: readonly { objectTypeId: string; primaryId: string }[]
191
+ }): Map<ObjectBatchKey, ObjectRow> {
192
+ const source = this.source
193
+ const result = new Map<ObjectBatchKey, ObjectRow>()
194
+ if (params.items.length === 0) return result
195
+
196
+ const items = params.items.map((item, batchIndex) => ({ ...item, batchIndex }))
197
+ const statement = source.wrapStatement(
198
+ `
199
+ SELECT
200
+ object.*,
201
+ CAST(json_extract(requested.value, '$.batchIndex') AS INTEGER) AS _batch_index
202
+ FROM json_each(?) AS requested
203
+ JOIN ${source.objectsTable} AS object
204
+ ON object.project_id = ?
205
+ AND object.object_type_id = json_extract(requested.value, '$.objectTypeId')
206
+ AND object.primary_id = json_extract(requested.value, '$.primaryId')
207
+ `,
208
+ [JSON.stringify(items), params.projectId]
209
+ )
210
+ const rows = this.db.query(statement.sql).all(...statement.args) as ObjectBatchDatabaseRow[]
211
+ const rowsByIndex = new Map(rows.map((row) => [row._batch_index, row]))
212
+ for (const [index, item] of params.items.entries()) {
213
+ const row = rowsByIndex.get(index)
214
+ if (row) result.set(objectBatchKey(item.objectTypeId, item.primaryId), rowToObject(row))
215
+ }
216
+ return result
217
+ }
218
+
219
+ listLinksBatch(params: {
220
+ projectId: string
221
+ direction?: LinkDirection
222
+ items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
223
+ }): Map<LinkBatchKey, ObjectLinkRow[]> {
224
+ const source = this.source
225
+ const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
226
+ if (params.items.length === 0) return result
227
+
228
+ const direction = params.direction ?? "outgoing"
229
+ const requestedType = "CAST(json_extract(requested.value, '$.objectTypeId') AS TEXT)"
230
+ const requestedId = "CAST(json_extract(requested.value, '$.objectId') AS TEXT)"
231
+ const requestedLink = "CAST(json_extract(requested.value, '$.linkId') AS TEXT)"
232
+ const outgoing = `stored.source_type_id = ${requestedType} AND stored.source_id = ${requestedId}`
233
+ const incoming = `stored.target_type_id = ${requestedType} AND stored.target_id = ${requestedId}`
234
+ const directionJoin =
235
+ direction === "both"
236
+ ? `((${outgoing}) OR (${incoming}))`
237
+ : direction === "incoming"
238
+ ? incoming
239
+ : outgoing
240
+ const items = params.items.map((item, batchIndex) => ({ ...item, batchIndex }))
241
+ const statement = source.wrapStatement(
242
+ `
243
+ SELECT
244
+ stored.*,
245
+ CAST(json_extract(requested.value, '$.batchIndex') AS INTEGER) AS _batch_index
246
+ FROM ${source.linksTable} AS stored
247
+ JOIN json_each(?) AS requested
248
+ ON ${directionJoin}
249
+ AND stored.link_id = ${requestedLink}
250
+ WHERE stored.project_id = ?
251
+ ORDER BY _batch_index, source_type_id, source_id, link_id, target_type_id, target_id
252
+ `,
253
+ [JSON.stringify(items), params.projectId]
254
+ )
255
+ const rows = this.db.query(statement.sql).all(...statement.args) as LinkBatchDatabaseRow[]
256
+ const rowsByIndex = params.items.map(() => new Map<string, ObjectLinkRow>())
257
+ for (const row of rows) {
258
+ const link = rowToLink(row)
259
+ rowsByIndex[row._batch_index]?.set(fullLinkIdentity(link), link)
260
+ }
261
+ for (const [index, item] of params.items.entries()) {
262
+ const links = [...(rowsByIndex[index]?.values() ?? [])]
263
+ if (links.length > 0) {
264
+ result.set(linkBatchKey(item.objectTypeId, item.objectId, item.linkId), links)
265
+ }
266
+ }
267
+ return result
268
+ }
269
+
270
+ queryLinks(params: QueryObjectLinksInput): QueryObjectLinksResult {
271
+ const source = this.source
272
+ if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
273
+ return { links: [], hasMore: false }
274
+ }
275
+
276
+ const requested = `
277
+ SELECT DISTINCT
278
+ json_extract(value, '$.objectTypeId') AS object_type_id,
279
+ json_extract(value, '$.primaryId') AS object_id
280
+ FROM json_each(?)
281
+ `
282
+ const linksTable = source.linksTable
283
+ const sourceJoin = `
284
+ SELECT link.*
285
+ FROM ${linksTable} AS link
286
+ JOIN (${requested}) AS 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 ${linksTable} AS link
294
+ JOIN (${requested}) AS 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 requestedJson = JSON.stringify(params.objectRefs)
306
+ const args: (string | number)[] =
307
+ params.direction === "both"
308
+ ? [requestedJson, params.projectId, requestedJson, params.projectId]
309
+ : [requestedJson, params.projectId]
310
+
311
+ const predicates: string[] = []
312
+ if (params.linkId !== undefined) {
313
+ predicates.push("link_id = ?")
314
+ args.push(params.linkId)
315
+ }
316
+ if (params.endpointObjectTypeIds !== undefined) {
317
+ const allowedTypes = JSON.stringify([...new Set(params.endpointObjectTypeIds)])
318
+ predicates.push(
319
+ "source_type_id IN (SELECT value FROM json_each(?))",
320
+ "target_type_id IN (SELECT value FROM json_each(?))"
321
+ )
322
+ args.push(allowedTypes, allowedTypes)
323
+ }
324
+ if (params.after) {
325
+ predicates.push(
326
+ "(source_type_id, source_id, link_id, target_type_id, target_id) > (?, ?, ?, ?, ?)"
327
+ )
328
+ args.push(...params.after)
329
+ }
330
+ args.push(params.limit + 1)
331
+
332
+ const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : ""
333
+ const statement = source.wrapStatement(
334
+ `
335
+ SELECT *
336
+ FROM (${incidentSql}) AS incident
337
+ ${whereSql}
338
+ ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
339
+ LIMIT ?
340
+ `,
341
+ args
342
+ )
343
+ const rows = this.db.query(statement.sql).all(...statement.args) as LinkDatabaseRow[]
344
+
345
+ return {
346
+ links: rows.slice(0, params.limit).map((row) => rowToLink(row)),
347
+ hasMore: rows.length > params.limit,
348
+ }
349
+ }
350
+
351
+ list(params: Parameters<ObjectReadStorage["list"]>[0]): {
352
+ objects: readonly ObjectRow[]
353
+ hasMore: boolean
354
+ total: number
355
+ } {
356
+ const source = this.source
357
+ let query = `SELECT * FROM ${source.objectsTable} WHERE project_id = ?`
358
+ const args: (string | number | null)[] = [params.projectId]
359
+
360
+ if (params.objectTypeId) {
361
+ if (typeof params.objectTypeId === "string") {
362
+ query += " AND object_type_id = ?"
363
+ args.push(params.objectTypeId)
364
+ } else {
365
+ query += ` AND object_type_id IN (${params.objectTypeId.map(() => "?").join(", ")})`
366
+ args.push(...params.objectTypeId)
367
+ }
368
+ }
369
+
370
+ if (params.primaryIdPrefix) {
371
+ query += " AND primary_id LIKE ?"
372
+ args.push(`${params.primaryIdPrefix}%`)
373
+ }
374
+
375
+ if (params.primaryIdSuffix) {
376
+ query += " AND primary_id LIKE ?"
377
+ args.push(`%${params.primaryIdSuffix}`)
378
+ }
379
+
380
+ if (params.updatedAfter) {
381
+ query += " AND updated_at >= ?"
382
+ args.push(params.updatedAfter.toISOString())
383
+ }
384
+
385
+ if (params.updatedBefore) {
386
+ query += " AND updated_at <= ?"
387
+ args.push(params.updatedBefore.toISOString())
388
+ }
389
+
390
+ if (params.createdAfter) {
391
+ query += " AND created_at >= ?"
392
+ args.push(params.createdAfter.toISOString())
393
+ }
394
+
395
+ if (params.createdBefore) {
396
+ query += " AND created_at <= ?"
397
+ args.push(params.createdBefore.toISOString())
398
+ }
399
+
400
+ // Get total count
401
+ const countStatement = source.wrapStatement(
402
+ `SELECT COUNT(*) as total FROM (${query}) AS filtered_objects`,
403
+ args
404
+ )
405
+ const countResult = this.db.query(countStatement.sql).get(...countStatement.args) as {
406
+ total: number
407
+ }
408
+ const total = countResult.total
409
+
410
+ const offset = params.offset ?? 0
411
+ const limit = params.limit ?? 50
412
+ if (limit === 0) return { objects: [], hasMore: offset < total, total }
413
+
414
+ // Add ordering
415
+ const orderBy = params.orderBy ?? "updatedAt"
416
+ const order = params.order ?? "desc"
417
+ const orderColumn =
418
+ orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
419
+ query += ` ORDER BY ${orderColumn} ${order.toUpperCase()}`
420
+
421
+ // Add pagination
422
+ query += " LIMIT ? OFFSET ?"
423
+ args.push(limit + 1, offset) // +1 to check for hasMore
424
+
425
+ const rowsStatement = source.wrapStatement(query, args)
426
+ const rows = this.db.query(rowsStatement.sql).all(...rowsStatement.args) as DatabaseRow[]
427
+ const hasMore = rows.length > limit
428
+ const objects = rows.slice(0, limit).map((row) => rowToObject(row))
429
+
430
+ return { objects, hasMore, total }
431
+ }
432
+ }
433
+
434
+ function fullLinkIdentity(link: ObjectLinkRow): string {
435
+ return JSON.stringify([
436
+ link.sourceTypeId,
437
+ link.sourceId,
438
+ link.linkId,
439
+ link.targetTypeId,
440
+ link.targetId,
441
+ ])
442
+ }
443
+
444
+ export function assertLinkQueryLimit(limit: number): void {
445
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
446
+ throw new Error("Object link query limit must be a positive safe integer.")
447
+ }
448
+ }
449
+
450
+ function readTotal(db: Database, compiled: CompiledObjectQuery): number {
451
+ const row = db.query(compiled.totalSql).get(...compiled.totalArgs) as { total: number }
452
+ return row.total
453
+ }
454
+
455
+ function readFacetBuckets(
456
+ db: Database,
457
+ compiled: CompiledObjectQuery,
458
+ facet: ObjectFacetRequest
459
+ ): { value: unknown; count: number }[] {
460
+ const path = sqliteJsonPath(facet.propertyId)
461
+ const rows = db
462
+ .query(
463
+ `
464
+ SELECT
465
+ json_type(input.properties, ?) AS value_type,
466
+ json_extract(input.properties, ?) AS value,
467
+ COUNT(*) AS count
468
+ FROM (${compiled.sql}) AS input
469
+ WHERE json_type(input.properties, ?) IS NOT NULL
470
+ GROUP BY value_type, value
471
+ ORDER BY count DESC, CAST(value AS TEXT) ASC
472
+ LIMIT ?
473
+ `
474
+ )
475
+ .all(path, path, ...compiled.args, path, facet.limit) as FacetDatabaseRow[]
476
+
477
+ return rows.map((row) => ({
478
+ value: sqliteFacetValue(row.value_type, row.value),
479
+ count: Number(row.count),
480
+ }))
481
+ }
482
+
483
+ function sqliteJsonPath(propertyId: string): string {
484
+ return `$."${propertyId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
485
+ }
486
+
487
+ function sqliteFacetValue(valueType: string | null, value: unknown): unknown {
488
+ if (valueType === "true") return true
489
+ if (valueType === "false") return false
490
+ if (valueType === "null") return null
491
+ return value
492
+ }
493
+
494
+ function existsProbeQuery(query: ObjectQuery): ObjectQuery {
495
+ return { kind: "limit", limit: 1, input: stripOuterRowShape(query) }
496
+ }
497
+
498
+ function stripOuterRowShape(query: ObjectQuery): ObjectQuery {
499
+ switch (query.kind) {
500
+ case "limit":
501
+ case "page":
502
+ case "project":
503
+ case "sort":
504
+ return stripOuterRowShape(query.input)
505
+ default:
506
+ return query
507
+ }
508
+ }
509
+
510
+ interface ObjectBatchDatabaseRow extends DatabaseRow {
511
+ _batch_index: number
512
+ }
513
+
514
+ interface PropertyPermissionBatchDatabaseRow {
515
+ _batch_index: number
516
+ }
517
+
518
+ interface FacetDatabaseRow {
519
+ value_type: string | null
520
+ value: unknown
521
+ count: number
522
+ }
523
+
524
+ interface LinkBatchDatabaseRow extends LinkDatabaseRow {
525
+ _batch_index: number
526
+ }
@@ -0,0 +1,123 @@
1
+ import type {
2
+ ExpandedLinkValue,
3
+ ExpandedObjectRow,
4
+ ObjectLinkRow,
5
+ ObjectRow,
6
+ ObjectRowLinks,
7
+ } from "@sixb/core/storage"
8
+
9
+ export function rowToObject(row: DatabaseRow): ObjectRow {
10
+ return {
11
+ projectId: row.project_id,
12
+ objectTypeId: row.object_type_id,
13
+ primaryId: row.primary_id,
14
+ properties: JSON.parse(row.properties),
15
+ createdAt: new Date(row.created_at),
16
+ updatedAt: new Date(row.updated_at),
17
+ version: row.version,
18
+ lastCommitId: row.last_commit_id,
19
+ }
20
+ }
21
+
22
+ /** Decode an expanded query row with the same shape as Core link hydration. */
23
+ export function queryRowToObject(row: ObjectQueryDatabaseRow): ObjectRow {
24
+ const base = rowToObject(row)
25
+ const links = reviveExpandedLinks(parseExpandColumn(row._expand))
26
+ return links ? { ...base, links } : base
27
+ }
28
+
29
+ export function rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
30
+ return {
31
+ projectId: row.project_id,
32
+ sourceTypeId: row.source_type_id,
33
+ sourceId: row.source_id,
34
+ linkId: row.link_id,
35
+ targetTypeId: row.target_type_id,
36
+ targetId: row.target_id,
37
+ ...(row.properties === null ? {} : { properties: JSON.parse(row.properties) }),
38
+ createdAt: new Date(row.created_at),
39
+ updatedAt: new Date(row.updated_at),
40
+ lastCommitId: row.last_commit_id,
41
+ }
42
+ }
43
+
44
+ // The `_expand` column comes back from SQLite as a JSON string (or null/absent
45
+ // when the query carried no expansion); parse it before reviving.
46
+ function parseExpandColumn(value: unknown): unknown {
47
+ if (typeof value !== "string") return value ?? undefined
48
+ return JSON.parse(value)
49
+ }
50
+
51
+ function reviveExpandedLinks(value: unknown): ObjectRowLinks | undefined {
52
+ if (!isPlainObject(value)) return undefined
53
+ const links: ObjectRowLinks = {}
54
+ for (const [linkId, raw] of Object.entries(value)) {
55
+ links[linkId] = reviveExpandedLinkValue(raw)
56
+ }
57
+ return links
58
+ }
59
+
60
+ // A "one" expansion arrives as a single object or null; a "many" expansion as an
61
+ // array (already ordered and trimmed in the database).
62
+ function reviveExpandedLinkValue(value: unknown): ExpandedLinkValue {
63
+ if (value === null || value === undefined) return null
64
+ if (Array.isArray(value)) return value.map(reviveExpandedRow)
65
+ return reviveExpandedRow(value)
66
+ }
67
+
68
+ // Revive one hydrated neighbour from `compileExpansionChildJson`: timestamp
69
+ // strings back to `Date`, null/empty link properties dropped (matching the
70
+ // fallback), and nested links recursed.
71
+ function reviveExpandedRow(value: unknown): ExpandedObjectRow {
72
+ const row = isPlainObject(value) ? value : {}
73
+ const expanded: ExpandedObjectRow = {
74
+ projectId: String(row.projectId),
75
+ objectTypeId: String(row.objectTypeId),
76
+ primaryId: String(row.primaryId),
77
+ properties: isPlainObject(row.properties) ? row.properties : {},
78
+ createdAt: new Date(row.createdAt as string),
79
+ updatedAt: new Date(row.updatedAt as string),
80
+ version: Number(row.version),
81
+ lastCommitId: String(row.lastCommitId),
82
+ }
83
+ if (isPlainObject(row.linkProperties) && Object.keys(row.linkProperties).length > 0) {
84
+ expanded.linkProperties = row.linkProperties
85
+ }
86
+ const links = reviveExpandedLinks(row.links)
87
+ if (links) expanded.links = links
88
+ return expanded
89
+ }
90
+
91
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value)
93
+ }
94
+
95
+ export interface DatabaseRow {
96
+ project_id: string
97
+ object_type_id: string
98
+ primary_id: string
99
+ properties: string
100
+ created_at: string
101
+ updated_at: string
102
+ version: number
103
+ last_commit_id: string
104
+ }
105
+
106
+ export interface ObjectQueryDatabaseRow extends DatabaseRow {
107
+ _cursor_properties?: string
108
+ /** `json_object(linkId, value, ...)` serialized text from an `expand` pushdown; absent otherwise. */
109
+ _expand?: string | null
110
+ }
111
+
112
+ export interface LinkDatabaseRow {
113
+ project_id: string
114
+ source_type_id: string
115
+ source_id: string
116
+ link_id: string
117
+ target_type_id: string
118
+ target_id: string
119
+ properties: string | null
120
+ created_at: string
121
+ updated_at: string
122
+ last_commit_id: string
123
+ }