@sixb/sqlite 0.0.1

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. package/src/workflow-run-storage.ts +1148 -0
@@ -0,0 +1,605 @@
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
+ LinkDirection,
13
+ ObjectFacetRequest,
14
+ ObjectLinkRow,
15
+ ObjectQueryCapabilities,
16
+ ObjectRow,
17
+ ObjectRowLinks,
18
+ ObjectStorage,
19
+ QueryObjectsInput,
20
+ QueryObjectsResult,
21
+ } from "@sixb/core/storage"
22
+ import { installFreshSqliteSchema } from "./migrations"
23
+ import { type CompiledObjectQuery, compileObjectQuery } from "./object-query-compiler"
24
+ import {
25
+ closeSqliteStoreConnection,
26
+ openSqliteStoreConnection,
27
+ type SqliteStoreConnection,
28
+ } from "./transactions"
29
+
30
+ export interface SqliteObjectStorageOptions {
31
+ /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
32
+ path?: string
33
+ /** Internal shared connection used by bundled SqliteStorage. */
34
+ connection?: SqliteStoreConnection
35
+ }
36
+
37
+ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
38
+ queryObjects: true,
39
+ countObjects: true,
40
+ existsObjects: true,
41
+ facetObjects: true,
42
+ nodes: {
43
+ start: true,
44
+ filter: true,
45
+ text: true,
46
+ sort: true,
47
+ limit: true,
48
+ page: true,
49
+ traverse: true,
50
+ set: true,
51
+ project: true,
52
+ expand: true,
53
+ },
54
+ predicateOps: {
55
+ and: true,
56
+ or: true,
57
+ not: true,
58
+ eq: true,
59
+ neq: true,
60
+ lt: true,
61
+ lte: true,
62
+ gt: true,
63
+ gte: true,
64
+ in: true,
65
+ exists: true,
66
+ contains: true,
67
+ },
68
+ sortKinds: {
69
+ property: true,
70
+ },
71
+ traversalDirections: {
72
+ outgoing: true,
73
+ incoming: true,
74
+ },
75
+ setOps: {
76
+ union: true,
77
+ intersect: true,
78
+ subtract: true,
79
+ },
80
+ scalarOperations: {
81
+ string: { equality: true, ordering: true },
82
+ uuid: { equality: true, ordering: true },
83
+ boolean: { equality: true },
84
+ integer: { equality: true, ordering: true },
85
+ double: { equality: true, ordering: true },
86
+ decimal: { equality: true },
87
+ date: { equality: true, ordering: true },
88
+ timestamp: { equality: true, ordering: true },
89
+ },
90
+ limits: {
91
+ totalCount: true,
92
+ stablePageTokens: true,
93
+ },
94
+ notes: [
95
+ "SQLite object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
96
+ "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
+ "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
+ "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
99
+ ],
100
+ }
101
+
102
+ /**
103
+ * SQLite-based ObjectStorage implementation.
104
+ *
105
+ * Stores object projections and links. V1 object-query IR pushdown covers the
106
+ * scalar JSON-property and link-traversal subset declared by queryCapabilities().
107
+ */
108
+ export class SqliteObjectStorage implements ObjectStorage {
109
+ private readonly connection: SqliteStoreConnection
110
+ private readonly db: Database
111
+
112
+ constructor(options: SqliteObjectStorageOptions = {}) {
113
+ this.connection = openSqliteStoreConnection(options)
114
+ this.db = this.connection.db
115
+
116
+ if (this.connection.installFreshSchema) {
117
+ installFreshSqliteSchema(this.db)
118
+ }
119
+ }
120
+
121
+ queryCapabilities(): ObjectQueryCapabilities {
122
+ return SQLITE_OBJECT_QUERY_CAPABILITIES
123
+ }
124
+
125
+ async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
126
+ const compiled = compileObjectQuery(params.projectId, params.query, {
127
+ includeTotal: params.includeTotal,
128
+ })
129
+ const total = params.includeTotal === false ? undefined : readTotal(this.db, compiled)
130
+ const rawRows = this.db.query(compiled.sql).all(...compiled.args) as ObjectQueryDatabaseRow[]
131
+ const rows = compiled.trimRows(rawRows) as readonly ObjectQueryDatabaseRow[]
132
+ const hasMore =
133
+ total === undefined && compiled.hasMoreProbe
134
+ ? compiled.hasMoreProbe.hasMore(
135
+ this.db.query(compiled.hasMoreProbe.sql).all(...compiled.hasMoreProbe.args).length
136
+ )
137
+ : compiled.hasMore(rawRows.length, total)
138
+
139
+ return {
140
+ objects: rows.map((row) => this.queryRowToObject(row)),
141
+ hasMore,
142
+ nextPageToken: compiled.nextPageToken(rows, rawRows.length),
143
+ ...(total === undefined ? {} : { total }),
144
+ }
145
+ }
146
+
147
+ async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
148
+ return {
149
+ count: readTotal(
150
+ this.db,
151
+ compileObjectQuery(params.projectId, stripOuterRowShape(params.query))
152
+ ),
153
+ }
154
+ }
155
+
156
+ async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
157
+ const compiled = compileObjectQuery(params.projectId, existsProbeQuery(params.query))
158
+ return { exists: this.db.query(compiled.sql).get(...compiled.args) !== null }
159
+ }
160
+
161
+ async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
162
+ const compiled = compileObjectQuery(params.projectId, stripOuterRowShape(params.query))
163
+ return {
164
+ facets: params.facets.map((facet) => ({
165
+ propertyId: facet.propertyId,
166
+ buckets: readFacetBuckets(this.db, compiled, facet),
167
+ })),
168
+ }
169
+ }
170
+
171
+ async getByPrimaryId(params: {
172
+ projectId: string
173
+ objectTypeId: string
174
+ primaryId: string
175
+ }): Promise<ObjectRow | null> {
176
+ const row = this.db
177
+ .query("SELECT * FROM objects WHERE project_id = ? AND object_type_id = ? AND primary_id = ?")
178
+ .get(params.projectId, params.objectTypeId, params.primaryId) as DatabaseRow | null
179
+
180
+ return row ? this.rowToObject(row) : null
181
+ }
182
+
183
+ async listLinks(params: {
184
+ projectId: string
185
+ objectTypeId: string
186
+ objectId: string
187
+ linkId?: string
188
+ direction?: LinkDirection
189
+ }): Promise<readonly ObjectLinkRow[]> {
190
+ const direction = params.direction ?? "outgoing"
191
+ const directionWhere =
192
+ direction === "incoming"
193
+ ? "target_type_id = ? AND target_id = ?"
194
+ : direction === "both"
195
+ ? "((source_type_id = ? AND source_id = ?) OR (target_type_id = ? AND target_id = ?))"
196
+ : "source_type_id = ? AND source_id = ?"
197
+ let query = `SELECT * FROM links WHERE project_id = ? AND ${directionWhere}`
198
+ const args: (string | number)[] =
199
+ direction === "both"
200
+ ? [
201
+ params.projectId,
202
+ params.objectTypeId,
203
+ params.objectId,
204
+ params.objectTypeId,
205
+ params.objectId,
206
+ ]
207
+ : [params.projectId, params.objectTypeId, params.objectId]
208
+
209
+ if (params.linkId) {
210
+ query += " AND link_id = ?"
211
+ args.push(params.linkId)
212
+ }
213
+
214
+ const rows = this.db.query(query).all(...args) as LinkDatabaseRow[]
215
+
216
+ return rows.map((row) => this.rowToLink(row))
217
+ }
218
+
219
+ async getByPrimaryIdBatch(params: {
220
+ projectId: string
221
+ items: readonly { objectTypeId: string; primaryId: string }[]
222
+ }): Promise<Map<string, ObjectRow>> {
223
+ const result = new Map<string, ObjectRow>()
224
+ if (params.items.length === 0) return result
225
+
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
+ }
238
+ }
239
+ return result
240
+ }
241
+
242
+ async listLinksBatch(params: {
243
+ projectId: string
244
+ items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
245
+ }): Promise<Map<string, ObjectLinkRow[]>> {
246
+ const result = new Map<string, ObjectLinkRow[]>()
247
+ if (params.items.length === 0) return result
248
+
249
+ const stmt = this.db.query(
250
+ "SELECT * FROM links WHERE project_id = ? AND source_type_id = ? AND source_id = ? AND link_id = ?"
251
+ )
252
+ for (const item of params.items) {
253
+ const rows = stmt.all(
254
+ params.projectId,
255
+ item.objectTypeId,
256
+ item.objectId,
257
+ item.linkId
258
+ ) as LinkDatabaseRow[]
259
+ if (rows.length > 0) {
260
+ result.set(
261
+ `${item.objectTypeId}:${item.objectId}:${item.linkId}`,
262
+ rows.map((r) => this.rowToLink(r))
263
+ )
264
+ }
265
+ }
266
+ return result
267
+ }
268
+
269
+ async listIncidentLinksBatch(params: {
270
+ projectId: string
271
+ items: readonly { objectTypeId: string; objectId: string }[]
272
+ }): 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()]
289
+ }
290
+
291
+ async listByPrimaryIdPage(params: {
292
+ projectId: string
293
+ objectTypeId: string
294
+ afterPrimaryId?: string
295
+ limit: number
296
+ }): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
297
+ assertReconciliationPageLimit(params.limit)
298
+ const rows = this.db
299
+ .query(
300
+ `
301
+ SELECT * FROM objects
302
+ WHERE project_id = ? AND object_type_id = ?
303
+ AND (? IS NULL OR primary_id > ?)
304
+ ORDER BY primary_id ASC
305
+ LIMIT ?
306
+ `
307
+ )
308
+ .all(
309
+ params.projectId,
310
+ params.objectTypeId,
311
+ params.afterPrimaryId ?? null,
312
+ params.afterPrimaryId ?? null,
313
+ params.limit + 1
314
+ ) as DatabaseRow[]
315
+ const hasMore = rows.length > params.limit
316
+ const objects = rows.slice(0, params.limit).map((row) => this.rowToObject(row))
317
+ const last = objects.at(-1)
318
+ return {
319
+ objects,
320
+ ...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
321
+ }
322
+ }
323
+
324
+ async list(params: {
325
+ projectId: string
326
+ objectTypeId?: string | readonly string[]
327
+ primaryIdPrefix?: string
328
+ primaryIdSuffix?: string
329
+ updatedAfter?: Date
330
+ updatedBefore?: Date
331
+ createdAfter?: Date
332
+ createdBefore?: Date
333
+ limit?: number
334
+ offset?: number
335
+ orderBy?: "createdAt" | "updatedAt" | "primaryId"
336
+ order?: "asc" | "desc"
337
+ }): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
338
+ let query = "SELECT * FROM objects WHERE project_id = ?"
339
+ const args: (string | number | null)[] = [params.projectId]
340
+
341
+ if (params.objectTypeId) {
342
+ if (typeof params.objectTypeId === "string") {
343
+ query += " AND object_type_id = ?"
344
+ args.push(params.objectTypeId)
345
+ } else {
346
+ query += ` AND object_type_id IN (${params.objectTypeId.map(() => "?").join(", ")})`
347
+ args.push(...params.objectTypeId)
348
+ }
349
+ }
350
+
351
+ if (params.primaryIdPrefix) {
352
+ query += " AND primary_id LIKE ?"
353
+ args.push(`${params.primaryIdPrefix}%`)
354
+ }
355
+
356
+ if (params.primaryIdSuffix) {
357
+ query += " AND primary_id LIKE ?"
358
+ args.push(`%${params.primaryIdSuffix}`)
359
+ }
360
+
361
+ if (params.updatedAfter) {
362
+ query += " AND updated_at >= ?"
363
+ args.push(params.updatedAfter.toISOString())
364
+ }
365
+
366
+ if (params.updatedBefore) {
367
+ query += " AND updated_at <= ?"
368
+ args.push(params.updatedBefore.toISOString())
369
+ }
370
+
371
+ if (params.createdAfter) {
372
+ query += " AND created_at >= ?"
373
+ args.push(params.createdAfter.toISOString())
374
+ }
375
+
376
+ if (params.createdBefore) {
377
+ query += " AND created_at <= ?"
378
+ args.push(params.createdBefore.toISOString())
379
+ }
380
+
381
+ // Get total count
382
+ const countResult = this.db.query(`SELECT COUNT(*) as total FROM (${query})`).get(...args) as {
383
+ total: number
384
+ }
385
+ const total = countResult.total
386
+
387
+ const offset = params.offset ?? 0
388
+ const limit = params.limit ?? 50
389
+ if (limit === 0) return { objects: [], hasMore: offset < total, total }
390
+
391
+ // Add ordering
392
+ const orderBy = params.orderBy ?? "updatedAt"
393
+ const order = params.order ?? "desc"
394
+ const orderColumn =
395
+ orderBy === "primaryId" ? "primary_id" : orderBy === "createdAt" ? "created_at" : "updated_at"
396
+ query += ` ORDER BY ${orderColumn} ${order.toUpperCase()}`
397
+
398
+ // Add pagination
399
+ query += " LIMIT ? OFFSET ?"
400
+ args.push(limit + 1, offset) // +1 to check for hasMore
401
+
402
+ const rows = this.db.query(query).all(...args) as DatabaseRow[]
403
+ const hasMore = rows.length > limit
404
+ const objects = rows.slice(0, limit).map((row) => this.rowToObject(row))
405
+
406
+ return { objects, hasMore, total }
407
+ }
408
+ private rowToObject(row: DatabaseRow): ObjectRow {
409
+ return {
410
+ projectId: row.project_id,
411
+ objectTypeId: row.object_type_id,
412
+ primaryId: row.primary_id,
413
+ properties: JSON.parse(row.properties),
414
+ createdAt: new Date(row.created_at),
415
+ updatedAt: new Date(row.updated_at),
416
+ version: row.version,
417
+ lastCommitId: row.last_commit_id,
418
+ }
419
+ }
420
+
421
+ // Map a query row, attaching `links` when an `expand` pushdown produced them.
422
+ // The base columns map as usual; `_expand` (a `json_object` serialized to TEXT)
423
+ // is parsed and revived into the runtime link shape the core executor's
424
+ // fallback also produces.
425
+ private queryRowToObject(row: ObjectQueryDatabaseRow): ObjectRow {
426
+ const base = this.rowToObject(row)
427
+ const links = reviveExpandedLinks(parseExpandColumn(row._expand))
428
+ return links ? { ...base, links } : base
429
+ }
430
+
431
+ private rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
432
+ return {
433
+ projectId: row.project_id,
434
+ sourceTypeId: row.source_type_id,
435
+ sourceId: row.source_id,
436
+ linkId: row.link_id,
437
+ targetTypeId: row.target_type_id,
438
+ targetId: row.target_id,
439
+ properties: row.properties ? JSON.parse(row.properties) : undefined,
440
+ createdAt: new Date(row.created_at),
441
+ updatedAt: new Date(row.updated_at),
442
+ lastCommitId: row.last_commit_id,
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Close the database connection.
448
+ */
449
+ close(): void {
450
+ closeSqliteStoreConnection(this.connection)
451
+ }
452
+ }
453
+
454
+ function assertReconciliationPageLimit(limit: number): void {
455
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
456
+ throw new Error("Object reconciliation page limit must be a positive safe integer.")
457
+ }
458
+ }
459
+
460
+ function readTotal(db: Database, compiled: CompiledObjectQuery): number {
461
+ const row = db.query(compiled.totalSql).get(...compiled.totalArgs) as { total: number }
462
+ return row.total
463
+ }
464
+
465
+ function readFacetBuckets(
466
+ db: Database,
467
+ compiled: CompiledObjectQuery,
468
+ facet: ObjectFacetRequest
469
+ ): { value: unknown; count: number }[] {
470
+ const path = sqliteJsonPath(facet.propertyId)
471
+ const rows = db
472
+ .query(
473
+ `
474
+ SELECT
475
+ json_type(input.properties, ?) AS value_type,
476
+ json_extract(input.properties, ?) AS value,
477
+ COUNT(*) AS count
478
+ FROM (${compiled.sql}) AS input
479
+ WHERE json_type(input.properties, ?) IS NOT NULL
480
+ GROUP BY value_type, value
481
+ ORDER BY count DESC, CAST(value AS TEXT) ASC
482
+ LIMIT ?
483
+ `
484
+ )
485
+ .all(path, path, ...compiled.args, path, facet.limit) as FacetDatabaseRow[]
486
+
487
+ return rows.map((row) => ({
488
+ value: sqliteFacetValue(row.value_type, row.value),
489
+ count: Number(row.count),
490
+ }))
491
+ }
492
+
493
+ function sqliteJsonPath(propertyId: string): string {
494
+ return `$."${propertyId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
495
+ }
496
+
497
+ function sqliteFacetValue(valueType: string | null, value: unknown): unknown {
498
+ if (valueType === "true") return true
499
+ if (valueType === "false") return false
500
+ if (valueType === "null") return null
501
+ return value
502
+ }
503
+
504
+ // The `_expand` column comes back from SQLite as a JSON string (or null/absent
505
+ // when the query carried no expansion); parse it before reviving.
506
+ function parseExpandColumn(value: unknown): unknown {
507
+ if (typeof value !== "string") return value ?? undefined
508
+ return JSON.parse(value)
509
+ }
510
+
511
+ function reviveExpandedLinks(value: unknown): ObjectRowLinks | undefined {
512
+ if (!isPlainObject(value)) return undefined
513
+ const links: ObjectRowLinks = {}
514
+ for (const [linkId, raw] of Object.entries(value)) {
515
+ links[linkId] = reviveExpandedLinkValue(raw)
516
+ }
517
+ return links
518
+ }
519
+
520
+ // A "one" expansion arrives as a single object or null; a "many" expansion as an
521
+ // array (already ordered and trimmed in the database).
522
+ function reviveExpandedLinkValue(value: unknown): ExpandedLinkValue {
523
+ if (value === null || value === undefined) return null
524
+ if (Array.isArray(value)) return value.map(reviveExpandedRow)
525
+ return reviveExpandedRow(value)
526
+ }
527
+
528
+ // Revive one hydrated neighbour from `compileExpansionChildJson`: timestamp
529
+ // strings back to `Date`, null/empty link properties dropped (matching the
530
+ // fallback), and nested links recursed.
531
+ function reviveExpandedRow(value: unknown): ExpandedObjectRow {
532
+ const row = isPlainObject(value) ? value : {}
533
+ const expanded: ExpandedObjectRow = {
534
+ projectId: String(row.projectId),
535
+ objectTypeId: String(row.objectTypeId),
536
+ primaryId: String(row.primaryId),
537
+ properties: isPlainObject(row.properties) ? row.properties : {},
538
+ createdAt: new Date(row.createdAt as string),
539
+ updatedAt: new Date(row.updatedAt as string),
540
+ version: Number(row.version),
541
+ lastCommitId: String(row.lastCommitId),
542
+ }
543
+ if (isPlainObject(row.linkProperties) && Object.keys(row.linkProperties).length > 0) {
544
+ expanded.linkProperties = row.linkProperties
545
+ }
546
+ const links = reviveExpandedLinks(row.links)
547
+ if (links) expanded.links = links
548
+ return expanded
549
+ }
550
+
551
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
552
+ return typeof value === "object" && value !== null && !Array.isArray(value)
553
+ }
554
+
555
+ function existsProbeQuery(query: ObjectQuery): ObjectQuery {
556
+ return { kind: "limit", limit: 1, input: stripOuterRowShape(query) }
557
+ }
558
+
559
+ function stripOuterRowShape(query: ObjectQuery): ObjectQuery {
560
+ switch (query.kind) {
561
+ case "limit":
562
+ case "page":
563
+ case "project":
564
+ case "sort":
565
+ return stripOuterRowShape(query.input)
566
+ default:
567
+ return query
568
+ }
569
+ }
570
+
571
+ interface DatabaseRow {
572
+ project_id: string
573
+ object_type_id: string
574
+ primary_id: string
575
+ properties: string
576
+ created_at: string
577
+ updated_at: string
578
+ version: number
579
+ last_commit_id: string
580
+ }
581
+
582
+ interface ObjectQueryDatabaseRow extends DatabaseRow {
583
+ _cursor_properties?: string
584
+ /** `json_object(linkId, value, ...)` serialized text from an `expand` pushdown; absent otherwise. */
585
+ _expand?: string | null
586
+ }
587
+
588
+ interface FacetDatabaseRow {
589
+ value_type: string | null
590
+ value: unknown
591
+ count: number
592
+ }
593
+
594
+ interface LinkDatabaseRow {
595
+ project_id: string
596
+ source_type_id: string
597
+ source_id: string
598
+ link_id: string
599
+ target_type_id: string
600
+ target_id: string
601
+ properties: string | null
602
+ created_at: string
603
+ updated_at: string
604
+ last_commit_id: string
605
+ }