@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
@@ -1,709 +0,0 @@
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
- ExpandedLinkValue,
9
- ExpandedObjectRow,
10
- FacetObjectsInput,
11
- FacetObjectsResult,
12
- LinkBatchKey,
13
- LinkDirection,
14
- ObjectBatchKey,
15
- ObjectFacetRequest,
16
- ObjectLinkRow,
17
- ObjectQueryCapabilities,
18
- ObjectRow,
19
- ObjectRowLinks,
20
- ObjectStorage,
21
- QueryObjectLinksInput,
22
- QueryObjectLinksResult,
23
- QueryObjectsInput,
24
- QueryObjectsResult,
25
- } from "@sixb/core/storage"
26
- import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
27
- import { installFreshSqliteSchema } from "./migrations"
28
- import { type CompiledObjectQuery, compileObjectQuery } from "./object-query-compiler"
29
- import {
30
- closeSqliteStoreConnection,
31
- openSqliteStoreConnection,
32
- type SqliteStoreConnection,
33
- } from "./transactions"
34
-
35
- export interface SqliteObjectStorageOptions {
36
- /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
37
- path?: string
38
- /** Internal shared connection used by bundled SqliteStorage. */
39
- connection?: SqliteStoreConnection
40
- }
41
-
42
- const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
43
- queryObjects: true,
44
- countObjects: true,
45
- existsObjects: true,
46
- facetObjects: true,
47
- nodes: {
48
- start: true,
49
- refs: true,
50
- filter: true,
51
- text: true,
52
- sort: true,
53
- limit: true,
54
- page: true,
55
- traverse: true,
56
- set: true,
57
- project: true,
58
- expand: true,
59
- },
60
- predicateOps: {
61
- and: true,
62
- or: true,
63
- not: true,
64
- eq: true,
65
- neq: true,
66
- lt: true,
67
- lte: true,
68
- gt: true,
69
- gte: true,
70
- in: true,
71
- exists: true,
72
- contains: true,
73
- },
74
- sortKinds: {
75
- property: true,
76
- },
77
- traversalDirections: {
78
- outgoing: true,
79
- incoming: true,
80
- },
81
- setOps: {
82
- union: true,
83
- intersect: true,
84
- subtract: true,
85
- },
86
- scalarOperations: {
87
- string: { equality: true, ordering: true },
88
- uuid: { equality: true, ordering: true },
89
- boolean: { equality: true },
90
- integer: { equality: true, ordering: true },
91
- double: { equality: true, ordering: true },
92
- decimal: { equality: true },
93
- date: { equality: true, ordering: true },
94
- timestamp: { equality: true, ordering: true },
95
- },
96
- limits: {
97
- totalCount: true,
98
- stablePageTokens: true,
99
- },
100
- notes: [
101
- "SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
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.",
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.",
104
- "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
105
- ],
106
- }
107
-
108
- /**
109
- * SQLite-based ObjectStorage implementation.
110
- *
111
- * Stores object projections and links. V1 object-query IR pushdown covers the
112
- * scalar JSON-property and link-traversal subset declared by queryCapabilities().
113
- */
114
- export class SqliteObjectStorage implements ObjectStorage {
115
- private readonly connection: SqliteStoreConnection
116
- private readonly db: Database
117
-
118
- constructor(options: SqliteObjectStorageOptions = {}) {
119
- this.connection = openSqliteStoreConnection(options)
120
- this.db = this.connection.db
121
-
122
- if (this.connection.installFreshSchema) {
123
- installFreshSqliteSchema(this.db)
124
- }
125
- }
126
-
127
- queryCapabilities(): ObjectQueryCapabilities {
128
- return SQLITE_OBJECT_QUERY_CAPABILITIES
129
- }
130
-
131
- async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
132
- const compiled = compileObjectQuery(params.projectId, params.query, {
133
- includeTotal: params.includeTotal,
134
- })
135
- const total = params.includeTotal === false ? undefined : readTotal(this.db, compiled)
136
- const rawRows = this.db.query(compiled.sql).all(...compiled.args) as ObjectQueryDatabaseRow[]
137
- const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
138
- const hasMore =
139
- total === undefined && compiled.hasMoreProbe
140
- ? compiled.hasMoreProbe.hasMore(
141
- this.db.query(compiled.hasMoreProbe.sql).all(...compiled.hasMoreProbe.args).length
142
- )
143
- : compiled.hasMore(rawRows.length, total)
144
-
145
- return {
146
- objects: rows.map((row) => this.queryRowToObject(row)),
147
- hasMore,
148
- nextPageToken: compiled.nextPageToken(rows, rawRows.length),
149
- ...(total === undefined ? {} : { total }),
150
- }
151
- }
152
-
153
- async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
154
- return {
155
- count: readTotal(
156
- this.db,
157
- compileObjectQuery(params.projectId, stripOuterRowShape(params.query))
158
- ),
159
- }
160
- }
161
-
162
- async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
163
- const compiled = compileObjectQuery(params.projectId, existsProbeQuery(params.query))
164
- return { exists: this.db.query(compiled.sql).get(...compiled.args) !== null }
165
- }
166
-
167
- async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
168
- const compiled = compileObjectQuery(params.projectId, stripOuterRowShape(params.query))
169
- return {
170
- facets: params.facets.map((facet) => ({
171
- propertyId: facet.propertyId,
172
- buckets: readFacetBuckets(this.db, compiled, facet),
173
- })),
174
- }
175
- }
176
-
177
- async getByPrimaryId(params: {
178
- projectId: string
179
- objectTypeId: string
180
- primaryId: string
181
- }): Promise<ObjectRow | null> {
182
- const row = this.db
183
- .query("SELECT * FROM objects WHERE project_id = ? AND object_type_id = ? AND primary_id = ?")
184
- .get(params.projectId, params.objectTypeId, params.primaryId) as DatabaseRow | null
185
-
186
- return row ? this.rowToObject(row) : null
187
- }
188
-
189
- async listLinks(params: {
190
- projectId: string
191
- objectTypeId: string
192
- objectId: string
193
- linkId?: string
194
- direction?: LinkDirection
195
- }): Promise<readonly ObjectLinkRow[]> {
196
- const direction = params.direction ?? "outgoing"
197
- const directionWhere =
198
- direction === "incoming"
199
- ? "target_type_id = ? AND target_id = ?"
200
- : direction === "both"
201
- ? "((source_type_id = ? AND source_id = ?) OR (target_type_id = ? AND target_id = ?))"
202
- : "source_type_id = ? AND source_id = ?"
203
- let query = `SELECT * FROM links WHERE project_id = ? AND ${directionWhere}`
204
- const args: (string | number)[] =
205
- direction === "both"
206
- ? [
207
- params.projectId,
208
- params.objectTypeId,
209
- params.objectId,
210
- params.objectTypeId,
211
- params.objectId,
212
- ]
213
- : [params.projectId, params.objectTypeId, params.objectId]
214
-
215
- if (params.linkId) {
216
- query += " AND link_id = ?"
217
- args.push(params.linkId)
218
- }
219
-
220
- const rows = this.db.query(query).all(...args) as LinkDatabaseRow[]
221
-
222
- return rows.map((row) => this.rowToLink(row))
223
- }
224
-
225
- async getByPrimaryIdBatch(params: {
226
- projectId: string
227
- items: readonly { objectTypeId: string; primaryId: string }[]
228
- }): Promise<Map<ObjectBatchKey, ObjectRow>> {
229
- const result = new Map<ObjectBatchKey, ObjectRow>()
230
- if (params.items.length === 0) return result
231
-
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))
246
- }
247
- return result
248
- }
249
-
250
- async listLinksBatch(params: {
251
- projectId: string
252
- items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
253
- }): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
254
- const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
255
- if (params.items.length === 0) return result
256
-
257
- const stmt = this.db.query(
258
- "SELECT * FROM links WHERE project_id = ? AND source_type_id = ? AND source_id = ? AND link_id = ?"
259
- )
260
- for (const item of params.items) {
261
- const rows = stmt.all(
262
- params.projectId,
263
- item.objectTypeId,
264
- item.objectId,
265
- item.linkId
266
- ) as LinkDatabaseRow[]
267
- if (rows.length > 0) {
268
- result.set(
269
- linkBatchKey(item.objectTypeId, item.objectId, item.linkId),
270
- rows.map((r) => this.rowToLink(r))
271
- )
272
- }
273
- }
274
- return result
275
- }
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
-
354
- async listIncidentLinksBatch(params: {
355
- projectId: string
356
- items: readonly { objectTypeId: string; objectId: string }[]
357
- }): Promise<readonly ObjectLinkRow[]> {
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))
387
- }
388
-
389
- async listByPrimaryIdPage(params: {
390
- projectId: string
391
- objectTypeId: string
392
- afterPrimaryId?: string
393
- limit: number
394
- }): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
395
- assertReconciliationPageLimit(params.limit)
396
- const rows = this.db
397
- .query(
398
- `
399
- SELECT * FROM objects
400
- WHERE project_id = ? AND object_type_id = ?
401
- AND (? IS NULL OR primary_id > ?)
402
- ORDER BY primary_id ASC
403
- LIMIT ?
404
- `
405
- )
406
- .all(
407
- params.projectId,
408
- params.objectTypeId,
409
- params.afterPrimaryId ?? null,
410
- params.afterPrimaryId ?? null,
411
- params.limit + 1
412
- ) as DatabaseRow[]
413
- const hasMore = rows.length > params.limit
414
- const objects = rows.slice(0, params.limit).map((row) => this.rowToObject(row))
415
- const last = objects.at(-1)
416
- return {
417
- objects,
418
- ...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
419
- }
420
- }
421
-
422
- async list(params: {
423
- projectId: string
424
- objectTypeId?: string | readonly string[]
425
- primaryIdPrefix?: string
426
- primaryIdSuffix?: string
427
- updatedAfter?: Date
428
- updatedBefore?: Date
429
- createdAfter?: Date
430
- createdBefore?: Date
431
- limit?: number
432
- offset?: number
433
- orderBy?: "createdAt" | "updatedAt" | "primaryId"
434
- order?: "asc" | "desc"
435
- }): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
436
- let query = "SELECT * FROM objects WHERE project_id = ?"
437
- const args: (string | number | null)[] = [params.projectId]
438
-
439
- if (params.objectTypeId) {
440
- if (typeof params.objectTypeId === "string") {
441
- query += " AND object_type_id = ?"
442
- args.push(params.objectTypeId)
443
- } else {
444
- query += ` AND object_type_id IN (${params.objectTypeId.map(() => "?").join(", ")})`
445
- args.push(...params.objectTypeId)
446
- }
447
- }
448
-
449
- if (params.primaryIdPrefix) {
450
- query += " AND primary_id LIKE ?"
451
- args.push(`${params.primaryIdPrefix}%`)
452
- }
453
-
454
- if (params.primaryIdSuffix) {
455
- query += " AND primary_id LIKE ?"
456
- args.push(`%${params.primaryIdSuffix}`)
457
- }
458
-
459
- if (params.updatedAfter) {
460
- query += " AND updated_at >= ?"
461
- args.push(params.updatedAfter.toISOString())
462
- }
463
-
464
- if (params.updatedBefore) {
465
- query += " AND updated_at <= ?"
466
- args.push(params.updatedBefore.toISOString())
467
- }
468
-
469
- if (params.createdAfter) {
470
- query += " AND created_at >= ?"
471
- args.push(params.createdAfter.toISOString())
472
- }
473
-
474
- if (params.createdBefore) {
475
- query += " AND created_at <= ?"
476
- args.push(params.createdBefore.toISOString())
477
- }
478
-
479
- // Get total count
480
- const countResult = this.db.query(`SELECT COUNT(*) as total FROM (${query})`).get(...args) as {
481
- total: number
482
- }
483
- const total = countResult.total
484
-
485
- const offset = params.offset ?? 0
486
- const limit = params.limit ?? 50
487
- if (limit === 0) return { objects: [], hasMore: offset < total, total }
488
-
489
- // Add ordering
490
- const orderBy = params.orderBy ?? "updatedAt"
491
- const order = params.order ?? "desc"
492
- const orderColumn =
493
- orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
494
- query += ` ORDER BY ${orderColumn} ${order.toUpperCase()}`
495
-
496
- // Add pagination
497
- query += " LIMIT ? OFFSET ?"
498
- args.push(limit + 1, offset) // +1 to check for hasMore
499
-
500
- const rows = this.db.query(query).all(...args) as DatabaseRow[]
501
- const hasMore = rows.length > limit
502
- const objects = rows.slice(0, limit).map((row) => this.rowToObject(row))
503
-
504
- return { objects, hasMore, total }
505
- }
506
- private rowToObject(row: DatabaseRow): ObjectRow {
507
- return {
508
- projectId: row.project_id,
509
- objectTypeId: row.object_type_id,
510
- primaryId: row.primary_id,
511
- properties: JSON.parse(row.properties),
512
- createdAt: new Date(row.created_at),
513
- updatedAt: new Date(row.updated_at),
514
- version: row.version,
515
- lastCommitId: row.last_commit_id,
516
- }
517
- }
518
-
519
- // Map a query row, attaching `links` when an `expand` pushdown produced them.
520
- // The base columns map as usual; `_expand` (a `json_object` serialized to TEXT)
521
- // is parsed and revived into the runtime link shape the core executor's
522
- // fallback also produces.
523
- private queryRowToObject(row: ObjectQueryDatabaseRow): ObjectRow {
524
- const base = this.rowToObject(row)
525
- const links = reviveExpandedLinks(parseExpandColumn(row._expand))
526
- return links ? { ...base, links } : base
527
- }
528
-
529
- private rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
530
- return {
531
- projectId: row.project_id,
532
- sourceTypeId: row.source_type_id,
533
- sourceId: row.source_id,
534
- linkId: row.link_id,
535
- targetTypeId: row.target_type_id,
536
- targetId: row.target_id,
537
- properties: row.properties ? JSON.parse(row.properties) : undefined,
538
- createdAt: new Date(row.created_at),
539
- updatedAt: new Date(row.updated_at),
540
- lastCommitId: row.last_commit_id,
541
- }
542
- }
543
-
544
- /**
545
- * Close the database connection.
546
- */
547
- close(): void {
548
- closeSqliteStoreConnection(this.connection)
549
- }
550
- }
551
-
552
- function assertReconciliationPageLimit(limit: number): void {
553
- if (!Number.isSafeInteger(limit) || limit <= 0) {
554
- throw new Error("Object reconciliation page limit must be a positive safe integer.")
555
- }
556
- }
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
-
564
- function readTotal(db: Database, compiled: CompiledObjectQuery): number {
565
- const row = db.query(compiled.totalSql).get(...compiled.totalArgs) as { total: number }
566
- return row.total
567
- }
568
-
569
- function readFacetBuckets(
570
- db: Database,
571
- compiled: CompiledObjectQuery,
572
- facet: ObjectFacetRequest
573
- ): { value: unknown; count: number }[] {
574
- const path = sqliteJsonPath(facet.propertyId)
575
- const rows = db
576
- .query(
577
- `
578
- SELECT
579
- json_type(input.properties, ?) AS value_type,
580
- json_extract(input.properties, ?) AS value,
581
- COUNT(*) AS count
582
- FROM (${compiled.sql}) AS input
583
- WHERE json_type(input.properties, ?) IS NOT NULL
584
- GROUP BY value_type, value
585
- ORDER BY count DESC, CAST(value AS TEXT) ASC
586
- LIMIT ?
587
- `
588
- )
589
- .all(path, path, ...compiled.args, path, facet.limit) as FacetDatabaseRow[]
590
-
591
- return rows.map((row) => ({
592
- value: sqliteFacetValue(row.value_type, row.value),
593
- count: Number(row.count),
594
- }))
595
- }
596
-
597
- function sqliteJsonPath(propertyId: string): string {
598
- return `$."${propertyId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
599
- }
600
-
601
- function sqliteFacetValue(valueType: string | null, value: unknown): unknown {
602
- if (valueType === "true") return true
603
- if (valueType === "false") return false
604
- if (valueType === "null") return null
605
- return value
606
- }
607
-
608
- // The `_expand` column comes back from SQLite as a JSON string (or null/absent
609
- // when the query carried no expansion); parse it before reviving.
610
- function parseExpandColumn(value: unknown): unknown {
611
- if (typeof value !== "string") return value ?? undefined
612
- return JSON.parse(value)
613
- }
614
-
615
- function reviveExpandedLinks(value: unknown): ObjectRowLinks | undefined {
616
- if (!isPlainObject(value)) return undefined
617
- const links: ObjectRowLinks = {}
618
- for (const [linkId, raw] of Object.entries(value)) {
619
- links[linkId] = reviveExpandedLinkValue(raw)
620
- }
621
- return links
622
- }
623
-
624
- // A "one" expansion arrives as a single object or null; a "many" expansion as an
625
- // array (already ordered and trimmed in the database).
626
- function reviveExpandedLinkValue(value: unknown): ExpandedLinkValue {
627
- if (value === null || value === undefined) return null
628
- if (Array.isArray(value)) return value.map(reviveExpandedRow)
629
- return reviveExpandedRow(value)
630
- }
631
-
632
- // Revive one hydrated neighbour from `compileExpansionChildJson`: timestamp
633
- // strings back to `Date`, null/empty link properties dropped (matching the
634
- // fallback), and nested links recursed.
635
- function reviveExpandedRow(value: unknown): ExpandedObjectRow {
636
- const row = isPlainObject(value) ? value : {}
637
- const expanded: ExpandedObjectRow = {
638
- projectId: String(row.projectId),
639
- objectTypeId: String(row.objectTypeId),
640
- primaryId: String(row.primaryId),
641
- properties: isPlainObject(row.properties) ? row.properties : {},
642
- createdAt: new Date(row.createdAt as string),
643
- updatedAt: new Date(row.updatedAt as string),
644
- version: Number(row.version),
645
- lastCommitId: String(row.lastCommitId),
646
- }
647
- if (isPlainObject(row.linkProperties) && Object.keys(row.linkProperties).length > 0) {
648
- expanded.linkProperties = row.linkProperties
649
- }
650
- const links = reviveExpandedLinks(row.links)
651
- if (links) expanded.links = links
652
- return expanded
653
- }
654
-
655
- function isPlainObject(value: unknown): value is Record<string, unknown> {
656
- return typeof value === "object" && value !== null && !Array.isArray(value)
657
- }
658
-
659
- function existsProbeQuery(query: ObjectQuery): ObjectQuery {
660
- return { kind: "limit", limit: 1, input: stripOuterRowShape(query) }
661
- }
662
-
663
- function stripOuterRowShape(query: ObjectQuery): ObjectQuery {
664
- switch (query.kind) {
665
- case "limit":
666
- case "page":
667
- case "project":
668
- case "sort":
669
- return stripOuterRowShape(query.input)
670
- default:
671
- return query
672
- }
673
- }
674
-
675
- interface DatabaseRow {
676
- project_id: string
677
- object_type_id: string
678
- primary_id: string
679
- properties: string
680
- created_at: string
681
- updated_at: string
682
- version: number
683
- last_commit_id: string
684
- }
685
-
686
- interface ObjectQueryDatabaseRow extends DatabaseRow {
687
- _cursor_properties?: string
688
- /** `json_object(linkId, value, ...)` serialized text from an `expand` pushdown; absent otherwise. */
689
- _expand?: string | null
690
- }
691
-
692
- interface FacetDatabaseRow {
693
- value_type: string | null
694
- value: unknown
695
- count: number
696
- }
697
-
698
- interface LinkDatabaseRow {
699
- project_id: string
700
- source_type_id: string
701
- source_id: string
702
- link_id: string
703
- target_type_id: string
704
- target_id: string
705
- properties: string | null
706
- created_at: string
707
- updated_at: string
708
- last_commit_id: string
709
- }