@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,1198 @@
1
+ import {
2
+ type ObjectExpansion,
3
+ type ObjectQuery,
4
+ ObjectQueryExecutionError,
5
+ type ObjectQueryPredicate,
6
+ type ObjectQuerySetOperation,
7
+ type ObjectQuerySortField,
8
+ } from "@sixb/core"
9
+
10
+ export type SqliteValue = string | number | bigint | boolean | null
11
+
12
+ export interface SqliteObjectQueryPageRow {
13
+ object_type_id: string
14
+ primary_id: string
15
+ properties: string
16
+ _cursor_properties?: string
17
+ }
18
+
19
+ export interface CompiledObjectQuery {
20
+ sql: string
21
+ args: SqliteValue[]
22
+ totalSql: string
23
+ totalArgs: SqliteValue[]
24
+ order: CompiledOrder
25
+ hasMoreProbe?: CompiledHasMoreProbe
26
+ hasMore(rowCount: number, total?: number): boolean
27
+ trimRows<T>(rows: readonly T[]): readonly T[]
28
+ nextPageToken(rows: readonly SqliteObjectQueryPageRow[], rowCount: number): string | undefined
29
+ }
30
+
31
+ interface CompiledHasMoreProbe {
32
+ sql: string
33
+ args: SqliteValue[]
34
+ hasMore(rowCount: number): boolean
35
+ }
36
+
37
+ interface CompileContext {
38
+ probeLimit: boolean
39
+ }
40
+
41
+ interface CompiledPredicate {
42
+ sql: string
43
+ args: SqliteValue[]
44
+ }
45
+
46
+ interface CompiledOrder {
47
+ sql: string
48
+ args: SqliteValue[]
49
+ fields: readonly CompiledOrderField[]
50
+ propertyColumn: string
51
+ }
52
+
53
+ type CompiledOrderField =
54
+ | {
55
+ kind: "property"
56
+ propertyId: string
57
+ direction: "asc" | "desc"
58
+ }
59
+ | {
60
+ kind: "column"
61
+ column: "object_type_id" | "primary_id"
62
+ direction: "asc" | "desc"
63
+ }
64
+
65
+ interface EncodedPageToken {
66
+ version: 1
67
+ order: readonly string[]
68
+ values: readonly EncodedCursorValue[]
69
+ }
70
+
71
+ interface EncodedCursorValue {
72
+ nullish: boolean
73
+ value?: unknown
74
+ }
75
+
76
+ const PAGE_TOKEN_PREFIX = "keyset:"
77
+ const exactContext: CompileContext = { probeLimit: false }
78
+
79
+ // Fallback fanout cap for a "many" expansion that arrives without an explicit
80
+ // limit. The core executor normally bakes a limit in before pushdown; this only
81
+ // bounds the degenerate uncapped case. Mirrors the PostgreSQL compiler.
82
+ const DEFAULT_EXPANSION_FANOUT = 1_000
83
+
84
+ export function compileObjectQuery(
85
+ projectId: string,
86
+ query: ObjectQuery,
87
+ options: { includeTotal?: boolean } = {}
88
+ ): CompiledObjectQuery {
89
+ const ctx: CompileContext = { probeLimit: options.includeTotal === false }
90
+ return compileObjectQueryInternal(projectId, query, ctx)
91
+ }
92
+
93
+ function compileObjectQueryInternal(
94
+ projectId: string,
95
+ query: ObjectQuery,
96
+ ctx: CompileContext
97
+ ): CompiledObjectQuery {
98
+ switch (query.kind) {
99
+ case "start":
100
+ return compileStart(projectId, query)
101
+ case "filter":
102
+ return compileFilter(projectId, query.input, query.predicate)
103
+ case "sort":
104
+ return compileSort(projectId, query.input, query.fields, ctx)
105
+ case "limit":
106
+ return compileLimit(projectId, query.input, query.limit, ctx)
107
+ case "page":
108
+ return compilePage(projectId, query.input, query.pageSize, query.pageToken)
109
+ case "traverse":
110
+ return compileTraversal(
111
+ projectId,
112
+ query.input,
113
+ query.linkId,
114
+ query.direction,
115
+ query.sourceObjectTypeId
116
+ )
117
+ case "set":
118
+ return compileSet(projectId, query.op, query.inputs)
119
+ case "project":
120
+ return compileProject(projectId, query.input, query.properties, ctx)
121
+ case "text":
122
+ return compileText(
123
+ projectId,
124
+ query.input,
125
+ query.query,
126
+ query.fields,
127
+ query.fieldsByObjectType
128
+ )
129
+ case "expand":
130
+ return compileExpand(projectId, query.input, query.expansions)
131
+ case "vector":
132
+ throw new Error(`[Sixb] SQLite object storage does not support query node '${query.kind}'`)
133
+ }
134
+ }
135
+
136
+ function compileStart(
137
+ projectId: string,
138
+ query: Extract<ObjectQuery, { kind: "start" }>
139
+ ): CompiledObjectQuery {
140
+ if (query.includeSubtypes === true) {
141
+ throw new Error("[Sixb] SQLite object storage does not support start.includeSubtypes")
142
+ }
143
+
144
+ const order = compileOrder(identityOrderFields())
145
+ const sql = `
146
+ SELECT *, properties AS _cursor_properties
147
+ FROM objects
148
+ WHERE project_id = ? AND object_type_id = ?
149
+ ORDER BY ${order.sql}
150
+ `
151
+ const args = [projectId, query.objectTypeId, ...order.args]
152
+
153
+ return {
154
+ sql,
155
+ args,
156
+ totalSql: "SELECT COUNT(*) as total FROM objects WHERE project_id = ? AND object_type_id = ?",
157
+ totalArgs: [projectId, query.objectTypeId],
158
+ order,
159
+ hasMore: () => false,
160
+ trimRows: identityRows,
161
+ nextPageToken: () => undefined,
162
+ }
163
+ }
164
+
165
+ function compileFilter(
166
+ projectId: string,
167
+ inputQuery: ObjectQuery,
168
+ predicateNode: ObjectQueryPredicate
169
+ ): CompiledObjectQuery {
170
+ const predicate = compilePredicate(predicateNode)
171
+ return compileWhere(projectId, inputQuery, predicate)
172
+ }
173
+
174
+ function compileWhere(
175
+ projectId: string,
176
+ inputQuery: ObjectQuery,
177
+ predicate: CompiledPredicate
178
+ ): CompiledObjectQuery {
179
+ const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
180
+ const sql = `
181
+ SELECT *
182
+ FROM (${input.sql}) AS input
183
+ WHERE ${predicate.sql}
184
+ ORDER BY ${input.order.sql}
185
+ `
186
+
187
+ return {
188
+ sql,
189
+ args: [...input.args, ...predicate.args, ...input.order.args],
190
+ totalSql: `
191
+ SELECT COUNT(*) as total
192
+ FROM (${input.sql}) AS input
193
+ WHERE ${predicate.sql}
194
+ `,
195
+ totalArgs: [...input.args, ...predicate.args],
196
+ order: input.order,
197
+ hasMore: () => false,
198
+ trimRows: identityRows,
199
+ nextPageToken: () => undefined,
200
+ }
201
+ }
202
+
203
+ function compileText(
204
+ projectId: string,
205
+ inputQuery: ObjectQuery,
206
+ query: string,
207
+ fields: readonly string[] | undefined,
208
+ fieldsByObjectType: Readonly<Record<string, readonly string[]>> | undefined
209
+ ): CompiledObjectQuery {
210
+ const predicate =
211
+ fields && fields.length > 0
212
+ ? compileTextPredicate(query, fields)
213
+ : compileScopedTextPredicate(query, fieldsByObjectType)
214
+
215
+ if (!predicate) {
216
+ throw new Error("[Sixb] SQLite object text search requires fields or resolved text defaults")
217
+ }
218
+ return compileWhere(projectId, inputQuery, predicate)
219
+ }
220
+
221
+ function compileSort(
222
+ projectId: string,
223
+ inputQuery: ObjectQuery,
224
+ fields: readonly ObjectQuerySortField[],
225
+ ctx: CompileContext
226
+ ): CompiledObjectQuery {
227
+ const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
228
+ const probeInput =
229
+ ctx.probeLimit && needsLimitProbe(inputQuery)
230
+ ? compileObjectQueryInternal(projectId, inputQuery, ctx)
231
+ : undefined
232
+ const order = compileOrder(sortOrderFields(fields))
233
+
234
+ return {
235
+ sql: `
236
+ SELECT
237
+ input.project_id,
238
+ input.object_type_id,
239
+ input.primary_id,
240
+ input.properties,
241
+ input.properties AS _cursor_properties,
242
+ input.created_at,
243
+ input.updated_at,
244
+ input.version,
245
+ input.last_commit_id
246
+ FROM (${input.sql}) AS input
247
+ ORDER BY ${order.sql}
248
+ `,
249
+ args: [...input.args, ...order.args],
250
+ totalSql: input.totalSql,
251
+ totalArgs: input.totalArgs,
252
+ order,
253
+ hasMoreProbe: probeInput ? hasMoreProbeFor(probeInput) : input.hasMoreProbe,
254
+ hasMore: input.hasMore,
255
+ trimRows: input.trimRows,
256
+ nextPageToken: input.nextPageToken,
257
+ }
258
+ }
259
+
260
+ function compileLimit(
261
+ projectId: string,
262
+ inputQuery: ObjectQuery,
263
+ rawLimit: number,
264
+ ctx: CompileContext
265
+ ): CompiledObjectQuery {
266
+ const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
267
+ const limit = Math.max(0, rawLimit)
268
+ const rowLimit = ctx.probeLimit ? limit + 1 : limit
269
+
270
+ return {
271
+ sql: `
272
+ SELECT *
273
+ FROM (${input.sql}) AS input
274
+ ORDER BY ${input.order.sql}
275
+ LIMIT ?
276
+ `,
277
+ args: [...input.args, ...input.order.args, rowLimit],
278
+ totalSql: `
279
+ SELECT COUNT(*) as total
280
+ FROM (${input.sql}) AS input
281
+ `,
282
+ totalArgs: input.args,
283
+ order: input.order,
284
+ hasMore: (rowCount, total) =>
285
+ total === undefined ? ctx.probeLimit && rowCount > limit : limit < total,
286
+ trimRows: ctx.probeLimit ? (rows) => rows.slice(0, limit) : identityRows,
287
+ nextPageToken: () => undefined,
288
+ }
289
+ }
290
+
291
+ function compilePage(
292
+ projectId: string,
293
+ inputQuery: ObjectQuery,
294
+ rawPageSize: number,
295
+ pageToken: string | undefined
296
+ ): CompiledObjectQuery {
297
+ const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
298
+ const pageSize = Math.max(0, rawPageSize)
299
+ const cursor = pageToken ? decodePageToken(pageToken, input.order.fields) : undefined
300
+ const cursorPredicate = cursor
301
+ ? compileKeysetPredicate(input.order.fields, cursor, input.order.propertyColumn)
302
+ : undefined
303
+ const whereSql = cursorPredicate ? `WHERE ${cursorPredicate.sql}` : ""
304
+
305
+ return {
306
+ sql: `
307
+ SELECT *
308
+ FROM (${input.sql}) AS input
309
+ ${whereSql}
310
+ ORDER BY ${input.order.sql}
311
+ LIMIT ?
312
+ `,
313
+ args: [...input.args, ...(cursorPredicate?.args ?? []), ...input.order.args, pageSize + 1],
314
+ totalSql: `
315
+ SELECT COUNT(*) as total
316
+ FROM (${input.sql}) AS input
317
+ `,
318
+ totalArgs: input.args,
319
+ order: input.order,
320
+ hasMore: (rowCount) => rowCount > pageSize,
321
+ trimRows: (rows) => rows.slice(0, pageSize),
322
+ nextPageToken: (rows, rowCount) => {
323
+ if (rowCount <= pageSize || rows.length === 0) return undefined
324
+ return encodePageToken(rows[rows.length - 1], input.order.fields)
325
+ },
326
+ }
327
+ }
328
+
329
+ function compileTraversal(
330
+ projectId: string,
331
+ inputQuery: ObjectQuery,
332
+ linkId: string,
333
+ direction: "outgoing" | "incoming",
334
+ sourceObjectTypeId?: string
335
+ ): CompiledObjectQuery {
336
+ const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
337
+ const outputAlias = direction === "outgoing" ? "target_object" : "source_object"
338
+ const joinSql =
339
+ direction === "outgoing"
340
+ ? `
341
+ JOIN links AS edge
342
+ ON edge.project_id = input.project_id
343
+ AND edge.source_type_id = input.object_type_id
344
+ AND edge.source_id = input.primary_id
345
+ AND edge.link_id = ?
346
+ JOIN objects AS target_object
347
+ ON target_object.project_id = edge.project_id
348
+ AND target_object.object_type_id = edge.target_type_id
349
+ AND target_object.primary_id = edge.target_id
350
+ `
351
+ : `
352
+ JOIN links AS edge
353
+ ON edge.project_id = input.project_id
354
+ AND edge.target_type_id = input.object_type_id
355
+ AND edge.target_id = input.primary_id
356
+ AND edge.link_id = ?${sourceObjectTypeId === undefined ? "" : "\n AND edge.source_type_id = ?"}
357
+ JOIN objects AS source_object
358
+ ON source_object.project_id = edge.project_id
359
+ AND source_object.object_type_id = edge.source_type_id
360
+ AND source_object.primary_id = edge.source_id
361
+ `
362
+ const order = compileOrder(identityOrderFields())
363
+ const qualifiedOrder = compileOrder(identityOrderFields(), outputAlias)
364
+ const sql = `
365
+ SELECT DISTINCT ${outputAlias}.*, ${outputAlias}.properties AS _cursor_properties
366
+ FROM (${input.sql}) AS input
367
+ ${joinSql}
368
+ ORDER BY ${qualifiedOrder.sql}
369
+ `
370
+ const args = [
371
+ ...input.args,
372
+ linkId,
373
+ ...(sourceObjectTypeId === undefined ? [] : [sourceObjectTypeId]),
374
+ ...qualifiedOrder.args,
375
+ ]
376
+
377
+ return {
378
+ sql,
379
+ args,
380
+ totalSql: `SELECT COUNT(*) as total FROM (${sql}) AS traversed`,
381
+ totalArgs: args,
382
+ order,
383
+ hasMore: () => false,
384
+ trimRows: identityRows,
385
+ nextPageToken: () => undefined,
386
+ }
387
+ }
388
+
389
+ /** Correlation handle for an expansion: the parent row's identity columns. */
390
+ interface ExpansionParent {
391
+ project: string
392
+ type: string
393
+ id: string
394
+ }
395
+
396
+ // `expand` attaches linked objects to each result row without changing which
397
+ // objects match (unlike `traverse`, which pivots to the targets). Each expansion
398
+ // becomes a correlated subquery that hydrates its links into a JSON value, and
399
+ // all expansions are folded into one `_expand` object column the row mapper reads
400
+ // back. The input set is otherwise untouched, so pagination/order pass through.
401
+ function compileExpand(
402
+ projectId: string,
403
+ inputQuery: ObjectQuery,
404
+ expansions: readonly ObjectExpansion[]
405
+ ): CompiledObjectQuery {
406
+ const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
407
+ const expand = compileExpansionsObject(
408
+ expansions,
409
+ { project: "input.project_id", type: "input.object_type_id", id: "input.primary_id" },
410
+ ""
411
+ )
412
+ const inputOrder = compileOrder(input.order.fields, "input", "input._cursor_properties")
413
+ const sql = `
414
+ SELECT input.*, ${expand.sql} AS _expand
415
+ FROM (${input.sql}) AS input
416
+ ORDER BY ${inputOrder.sql}
417
+ `
418
+
419
+ return {
420
+ sql,
421
+ args: [...expand.args, ...input.args, ...inputOrder.args],
422
+ totalSql: input.totalSql,
423
+ totalArgs: input.totalArgs,
424
+ order: input.order,
425
+ hasMoreProbe: input.hasMoreProbe,
426
+ hasMore: input.hasMore,
427
+ trimRows: input.trimRows,
428
+ nextPageToken: input.nextPageToken,
429
+ }
430
+ }
431
+
432
+ // `json_object(linkId, value, ...)` over a set of expansions sharing one parent.
433
+ // Link ids are user data, so they ride as parameters. Each value is a correlated
434
+ // subquery whose JSON subtype is lost crossing the subquery boundary, so it is
435
+ // re-parsed with `json(...)` to embed as nested JSON rather than a quoted string.
436
+ // Used both for the outer `_expand` column and for each child's nested `links`.
437
+ function compileExpansionsObject(
438
+ expansions: readonly ObjectExpansion[],
439
+ parent: ExpansionParent,
440
+ pathPrefix: string
441
+ ): CompiledPredicate {
442
+ const parts: string[] = []
443
+ const args: SqliteValue[] = []
444
+ expansions.forEach((expansion, index) => {
445
+ const path = pathPrefix === "" ? `${index}` : `${pathPrefix}_${index}`
446
+ const value = compileExpansionValue(expansion, parent, path)
447
+ parts.push("?", `json(${value.sql})`)
448
+ args.push(expansion.linkId, ...value.args)
449
+ })
450
+ return { sql: `json_object(${parts.join(", ")})`, args }
451
+ }
452
+
453
+ // One expansion's hydrated value. `row_number()` numbers a parent's links by the
454
+ // order from `compileExpansionOrder` (the expansion's `orderBy` against the target
455
+ // object's properties, then an identity tiebreak); `WHERE _ord <= N` keeps the
456
+ // top-N per parent in-DB; each retained neighbour becomes a JSON object; and the
457
+ // value is an ordered array ("many") or the first element or null ("one"). The
458
+ // cardinality is core-resolved before pushdown.
459
+ function compileExpansionValue(
460
+ expansion: ObjectExpansion,
461
+ parent: ExpansionParent,
462
+ path: string
463
+ ): CompiledPredicate {
464
+ const edge = `edge_${path}`
465
+ const target = `tgt_${path}`
466
+ const ranked = `ranked_${path}`
467
+ const incoming = expansion.direction === "incoming"
468
+ // The hydrated neighbour is the link target (outgoing) or source (incoming);
469
+ // the parent sits on the opposite end, mirroring `compileTraversal`.
470
+ const neighborType = incoming ? `${edge}.source_type_id` : `${edge}.target_type_id`
471
+ const neighborId = incoming ? `${edge}.source_id` : `${edge}.target_id`
472
+ const parentType = incoming ? `${edge}.target_type_id` : `${edge}.source_type_id`
473
+ const parentId = incoming ? `${edge}.target_id` : `${edge}.source_id`
474
+
475
+ const child = compileExpansionChildJson(expansion, edge, target, path)
476
+ const order = compileExpansionOrder(expansion, target, neighborType, neighborId)
477
+
478
+ const whereParts = [
479
+ `${edge}.project_id = ${parent.project}`,
480
+ `${parentType} = ${parent.type}`,
481
+ `${parentId} = ${parent.id}`,
482
+ `${edge}.link_id = ?`,
483
+ ]
484
+ const whereArgs: SqliteValue[] = [expansion.linkId]
485
+ if (incoming && expansion.sourceObjectTypeId !== undefined) {
486
+ whereParts.push(`${edge}.source_type_id = ?`)
487
+ whereArgs.push(expansion.sourceObjectTypeId)
488
+ }
489
+
490
+ const inner = `
491
+ SELECT ${child.sql} AS elem, row_number() OVER (ORDER BY ${order.sql}) AS _ord
492
+ FROM links AS ${edge}
493
+ JOIN objects AS ${target}
494
+ ON ${target}.project_id = ${edge}.project_id
495
+ AND ${target}.object_type_id = ${neighborType}
496
+ AND ${target}.primary_id = ${neighborId}
497
+ WHERE ${whereParts.join(" AND ")}
498
+ `
499
+ const innerArgs = [...child.args, ...order.args, ...whereArgs]
500
+
501
+ if (expansion.cardinality === "one") {
502
+ return {
503
+ sql: `(SELECT ${ranked}.elem FROM (${inner}) AS ${ranked} WHERE ${ranked}._ord = 1)`,
504
+ args: innerArgs,
505
+ }
506
+ }
507
+
508
+ // The fanout cap is baked into `limit` by the core executor before pushdown.
509
+ // Each element is re-parsed with `json(...)` so the aggregate embeds objects
510
+ // rather than the quoted JSON text the subquery column hands back.
511
+ const limit = expansion.limit ?? DEFAULT_EXPANSION_FANOUT
512
+ return {
513
+ sql: `COALESCE((SELECT json_group_array(json(${ranked}.elem) ORDER BY ${ranked}._ord) FROM (${inner}) AS ${ranked} WHERE ${ranked}._ord <= ?), '[]')`,
514
+ args: [...innerArgs, limit],
515
+ }
516
+ }
517
+
518
+ // Build one hydrated neighbour as an `ExpandedObjectRow`-shaped JSON object. The
519
+ // JSON-valued columns (`properties`, the edge's `linkProperties`) are re-parsed
520
+ // with `json(...)` so they embed as JSON rather than strings; `linkProperties`
521
+ // always rides along (the mapper drops it when null/empty), and nested expansions
522
+ // recurse under `links`, correlated on this neighbour.
523
+ function compileExpansionChildJson(
524
+ expansion: ObjectExpansion,
525
+ edge: string,
526
+ target: string,
527
+ path: string
528
+ ): CompiledPredicate {
529
+ const fields = [
530
+ `'projectId', ${target}.project_id`,
531
+ `'objectTypeId', ${target}.object_type_id`,
532
+ `'primaryId', ${target}.primary_id`,
533
+ `'properties', json(${target}.properties)`,
534
+ `'createdAt', ${target}.created_at`,
535
+ `'updatedAt', ${target}.updated_at`,
536
+ `'version', ${target}.version`,
537
+ `'lastCommitId', ${target}.last_commit_id`,
538
+ `'linkProperties', json(${edge}.properties)`,
539
+ ]
540
+ const args: SqliteValue[] = []
541
+
542
+ if (expansion.expand && expansion.expand.length > 0) {
543
+ const nested = compileExpansionsObject(
544
+ expansion.expand,
545
+ {
546
+ project: `${target}.project_id`,
547
+ type: `${target}.object_type_id`,
548
+ id: `${target}.primary_id`,
549
+ },
550
+ path
551
+ )
552
+ fields.push(`'links', ${nested.sql}`)
553
+ args.push(...nested.args)
554
+ }
555
+
556
+ return { sql: `json_object(${fields.join(", ")})`, args }
557
+ }
558
+
559
+ // Order a parent's links the way the fallback's per-parent trim does: each
560
+ // property sorts nulls last with the requested direction (against the neighbour's
561
+ // JSON properties), then a deterministic identity tiebreak on the neighbour.
562
+ function compileExpansionOrder(
563
+ expansion: ObjectExpansion,
564
+ target: string,
565
+ neighborType: string,
566
+ neighborId: string
567
+ ): CompiledPredicate {
568
+ const propertyColumn = `${target}.properties`
569
+ const clauses: string[] = []
570
+ const args: SqliteValue[] = []
571
+
572
+ for (const field of expansion.orderBy ?? []) {
573
+ // Relevance is a no-op in the fallback comparator; mirror that by skipping it.
574
+ if (field.kind !== "property") continue
575
+ if (field.scalarKind === "decimal") {
576
+ throw new ObjectQueryExecutionError(
577
+ "exact_decimal_not_supported",
578
+ "SQLite object storage cannot push down exact decimal expansion sorting",
579
+ "$.expand.orderBy"
580
+ )
581
+ }
582
+ const direction = field.direction === "desc" ? "DESC" : "ASC"
583
+ const path = jsonPath(field.propertyId)
584
+ clauses.push(
585
+ `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END ASC`,
586
+ `${jsonValueExpression(propertyColumn)} ${direction}`
587
+ )
588
+ args.push(path, path, path)
589
+ }
590
+
591
+ clauses.push(`${neighborType} ASC`, `${neighborId} ASC`)
592
+ return { sql: clauses.join(", "), args }
593
+ }
594
+
595
+ function compileSet(
596
+ projectId: string,
597
+ op: ObjectQuerySetOperation,
598
+ inputs: readonly ObjectQuery[]
599
+ ): CompiledObjectQuery {
600
+ if (inputs.length === 0) {
601
+ const order = compileOrder(identityOrderFields())
602
+ return {
603
+ sql: `
604
+ SELECT *
605
+ FROM objects
606
+ WHERE 1 = 0
607
+ ORDER BY ${order.sql}
608
+ `,
609
+ args: order.args,
610
+ totalSql: "SELECT 0 as total",
611
+ totalArgs: [],
612
+ order,
613
+ hasMore: () => false,
614
+ trimRows: identityRows,
615
+ nextPageToken: () => undefined,
616
+ }
617
+ }
618
+
619
+ const compiledInputs = inputs.map((input) =>
620
+ compileObjectQueryInternal(projectId, input, exactContext)
621
+ )
622
+ const identities = compileSetIdentities(op, compiledInputs)
623
+ const order = compileOrder(identityOrderFields())
624
+ const selectedOrder = compileOrder(identityOrderFields(), "selected")
625
+ const sql = `
626
+ SELECT selected.*, selected.properties AS _cursor_properties
627
+ FROM (${identities.sql}) AS ids
628
+ JOIN objects AS selected
629
+ ON selected.project_id = ?
630
+ AND selected.object_type_id = ids.object_type_id
631
+ AND selected.primary_id = ids.primary_id
632
+ ORDER BY ${selectedOrder.sql}
633
+ `
634
+ const args = [...identities.args, projectId, ...selectedOrder.args]
635
+
636
+ return {
637
+ sql,
638
+ args,
639
+ totalSql: `SELECT COUNT(*) as total FROM (${identities.sql}) AS ids`,
640
+ totalArgs: identities.args,
641
+ order,
642
+ hasMore: () => false,
643
+ trimRows: identityRows,
644
+ nextPageToken: () => undefined,
645
+ }
646
+ }
647
+
648
+ function compileProject(
649
+ projectId: string,
650
+ inputQuery: ObjectQuery,
651
+ properties: readonly string[] | undefined,
652
+ ctx: CompileContext
653
+ ): CompiledObjectQuery {
654
+ const input = compileObjectQueryInternal(projectId, inputQuery, ctx)
655
+ if (!properties) return input
656
+
657
+ const projection = compileProjectionExpression(properties)
658
+ const inputOrder = compileOrder(input.order.fields, "input", "input._cursor_properties")
659
+ const outputOrder = compileOrder(input.order.fields, undefined, "_cursor_properties")
660
+
661
+ return {
662
+ sql: `
663
+ SELECT
664
+ input.project_id,
665
+ input.object_type_id,
666
+ input.primary_id,
667
+ ${projection.sql} AS properties,
668
+ input._cursor_properties AS _cursor_properties,
669
+ input.created_at,
670
+ input.updated_at,
671
+ input.version,
672
+ input.last_commit_id
673
+ FROM (${input.sql}) AS input
674
+ ORDER BY ${inputOrder.sql}
675
+ `,
676
+ args: [...projection.args, ...input.args, ...inputOrder.args],
677
+ totalSql: input.totalSql,
678
+ totalArgs: input.totalArgs,
679
+ order: outputOrder,
680
+ hasMoreProbe: input.hasMoreProbe,
681
+ hasMore: input.hasMore,
682
+ trimRows: input.trimRows,
683
+ nextPageToken: input.nextPageToken,
684
+ }
685
+ }
686
+
687
+ function needsLimitProbe(query: ObjectQuery): boolean {
688
+ switch (query.kind) {
689
+ case "limit":
690
+ return true
691
+ case "project":
692
+ case "sort":
693
+ return needsLimitProbe(query.input)
694
+ default:
695
+ return false
696
+ }
697
+ }
698
+
699
+ function hasMoreProbeFor(compiled: CompiledObjectQuery): CompiledHasMoreProbe {
700
+ return (
701
+ compiled.hasMoreProbe ?? {
702
+ sql: compiled.sql,
703
+ args: compiled.args,
704
+ hasMore: (rowCount) => compiled.hasMore(rowCount, undefined),
705
+ }
706
+ )
707
+ }
708
+
709
+ function compileSetIdentities(
710
+ op: ObjectQuerySetOperation,
711
+ inputs: readonly CompiledObjectQuery[]
712
+ ): CompiledPredicate {
713
+ const operator = op === "union" ? "UNION" : op === "intersect" ? "INTERSECT" : "EXCEPT"
714
+ const identitySqls = inputs.map(
715
+ (input) => `SELECT object_type_id, primary_id FROM (${input.sql}) AS input`
716
+ )
717
+
718
+ return {
719
+ sql: identitySqls.join(` ${operator} `),
720
+ args: inputs.flatMap((input) => input.args),
721
+ }
722
+ }
723
+
724
+ function compileProjectionExpression(properties: readonly string[]): CompiledPredicate {
725
+ if (properties.length === 0) return { sql: "json('{}')", args: [] }
726
+
727
+ return {
728
+ sql: `
729
+ COALESCE(
730
+ (
731
+ SELECT json_group_object(projected.key, projected.value)
732
+ FROM json_each(input.properties) AS projected
733
+ WHERE projected.key IN (${properties.map(() => "?").join(", ")})
734
+ ),
735
+ json('{}')
736
+ )
737
+ `,
738
+ args: [...properties],
739
+ }
740
+ }
741
+
742
+ function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
743
+ if (
744
+ "scalarKind" in predicate &&
745
+ predicate.scalarKind === "decimal" &&
746
+ (predicate.op === "lt" ||
747
+ predicate.op === "lte" ||
748
+ predicate.op === "gt" ||
749
+ predicate.op === "gte")
750
+ ) {
751
+ throw new ObjectQueryExecutionError(
752
+ "exact_decimal_not_supported",
753
+ "SQLite object storage cannot push down exact decimal predicates",
754
+ "$.predicate"
755
+ )
756
+ }
757
+
758
+ switch (predicate.op) {
759
+ case "and":
760
+ case "or": {
761
+ const items = predicate.items.map(compilePredicate)
762
+ const joiner = predicate.op === "and" ? " AND " : " OR "
763
+ return {
764
+ sql: `(${items.map((item) => item.sql).join(joiner)})`,
765
+ args: items.flatMap((item) => item.args),
766
+ }
767
+ }
768
+ case "not": {
769
+ const item = compilePredicate(predicate.item)
770
+ return negatePredicate(item)
771
+ }
772
+ case "eq":
773
+ return compileEqualityPredicate(predicate.propertyId, "eq", predicate.value)
774
+ case "neq":
775
+ return compileEqualityPredicate(predicate.propertyId, "neq", predicate.value)
776
+ case "lt":
777
+ case "lte":
778
+ case "gt":
779
+ case "gte": {
780
+ const op = sqlComparisonOperator(predicate.op)
781
+ return {
782
+ sql: `${jsonValueExpression()} ${op} ?`,
783
+ args: [jsonPath(predicate.propertyId), sqlValue(predicate.value)],
784
+ }
785
+ }
786
+ case "in":
787
+ return compileInPredicate(predicate.propertyId, predicate.values)
788
+ case "exists": {
789
+ const sql = `${jsonTypeExpression()} IS NOT NULL`
790
+ const item: CompiledPredicate = { sql, args: [jsonPath(predicate.propertyId)] }
791
+ return predicate.value ? item : negatePredicate(item)
792
+ }
793
+ case "contains":
794
+ return compileContainsPredicate(predicate.propertyId, predicate.value)
795
+ }
796
+ }
797
+
798
+ function negatePredicate(predicate: CompiledPredicate): CompiledPredicate {
799
+ return {
800
+ sql: `(NOT COALESCE((${predicate.sql}), 0))`,
801
+ args: predicate.args,
802
+ }
803
+ }
804
+
805
+ function compileEqualityPredicate(
806
+ propertyId: string,
807
+ op: "eq" | "neq",
808
+ value: unknown
809
+ ): CompiledPredicate {
810
+ const path = jsonPath(propertyId)
811
+ if (value === null) {
812
+ const sql = `${jsonTypeExpression()} = 'null'`
813
+ const item: CompiledPredicate = { sql, args: [path] }
814
+ return op === "eq" ? item : negatePredicate(item)
815
+ }
816
+
817
+ if (op === "eq") {
818
+ return {
819
+ sql: `${jsonValueExpression()} = ?`,
820
+ args: [path, sqlValue(value)],
821
+ }
822
+ }
823
+
824
+ // JS predicate semantics treat missing/null properties as not equal to any
825
+ // non-null scalar. SQL three-valued null logic needs those cases explicit.
826
+ return {
827
+ sql: `(${jsonTypeExpression()} IS NULL OR ${jsonTypeExpression()} = 'null' OR ${jsonValueExpression()} != ?)`,
828
+ args: [path, path, path, sqlValue(value)],
829
+ }
830
+ }
831
+
832
+ function compileInPredicate(propertyId: string, values: readonly unknown[]): CompiledPredicate {
833
+ if (values.length === 0) return { sql: "0 = 1", args: [] }
834
+
835
+ const path = jsonPath(propertyId)
836
+ const nonNullValues = values.filter((value) => value !== null)
837
+ const clauses: string[] = []
838
+ const args: SqliteValue[] = []
839
+
840
+ if (values.length !== nonNullValues.length) {
841
+ clauses.push(`${jsonTypeExpression()} = 'null'`)
842
+ args.push(path)
843
+ }
844
+
845
+ if (nonNullValues.length > 0) {
846
+ clauses.push(`${jsonValueExpression()} IN (${nonNullValues.map(() => "?").join(", ")})`)
847
+ args.push(path, ...nonNullValues.map(sqlValue))
848
+ }
849
+
850
+ return {
851
+ sql: `(${clauses.join(" OR ")})`,
852
+ args,
853
+ }
854
+ }
855
+
856
+ function compileContainsPredicate(propertyId: string, value: unknown): CompiledPredicate {
857
+ const path = jsonPath(propertyId)
858
+ const clauses: string[] = []
859
+ const args: SqliteValue[] = []
860
+
861
+ if (typeof value === "string") {
862
+ clauses.push(`(${jsonTypeExpression()} = 'text' AND instr(${jsonValueExpression()}, ?) > 0)`)
863
+ args.push(path, path, value)
864
+ }
865
+
866
+ clauses.push(
867
+ `(${jsonTypeExpression()} = 'array' AND EXISTS (SELECT 1 FROM json_each(properties, ?) AS item WHERE item.value IS ?))`
868
+ )
869
+ args.push(path, path, sqlValue(value))
870
+
871
+ if (typeof value === "string") {
872
+ clauses.push(
873
+ `(${jsonTypeExpression()} = 'object' AND EXISTS (SELECT 1 FROM json_each(properties, ?) AS item WHERE item.key = ?))`
874
+ )
875
+ args.push(path, path, value)
876
+ }
877
+
878
+ return { sql: `(${clauses.join(" OR ")})`, args }
879
+ }
880
+
881
+ function compileTextPredicate(query: string, fields: readonly string[]): CompiledPredicate {
882
+ const terms = tokenize(query)
883
+ if (terms.length === 0) return { sql: "0 = 1", args: [] }
884
+
885
+ const clauses = terms.map(() => {
886
+ const fieldClauses = fields.map(
887
+ () => `instr(lower(COALESCE(CAST(${jsonValueExpression()} AS TEXT), '')), ?) > 0`
888
+ )
889
+ return `(${fieldClauses.join(" OR ")})`
890
+ })
891
+ const args = terms.flatMap((term) => fields.flatMap((field) => [jsonPath(field), term]))
892
+
893
+ return { sql: `(${clauses.join(" AND ")})`, args }
894
+ }
895
+
896
+ function compileScopedTextPredicate(
897
+ query: string,
898
+ fieldsByObjectType: Readonly<Record<string, readonly string[]>> | undefined
899
+ ): CompiledPredicate | null {
900
+ const scopedPredicates: CompiledPredicate[] = []
901
+
902
+ for (const [objectTypeId, fields] of Object.entries(fieldsByObjectType ?? {})) {
903
+ if (fields.length === 0) continue
904
+ const predicate = compileTextPredicate(query, fields)
905
+ scopedPredicates.push({
906
+ sql: `(object_type_id = ? AND ${predicate.sql})`,
907
+ args: [objectTypeId, ...predicate.args],
908
+ })
909
+ }
910
+
911
+ if (scopedPredicates.length === 0) return null
912
+
913
+ return {
914
+ sql: `(${scopedPredicates.map((predicate) => predicate.sql).join(" OR ")})`,
915
+ args: scopedPredicates.flatMap((predicate) => predicate.args),
916
+ }
917
+ }
918
+
919
+ function compileOrder(
920
+ fields: readonly CompiledOrderField[],
921
+ qualifier?: string,
922
+ propertyColumn = "properties"
923
+ ): CompiledOrder {
924
+ const clauses: string[] = []
925
+ const args: SqliteValue[] = []
926
+
927
+ for (const field of fields) {
928
+ if (field.kind === "column") {
929
+ clauses.push(`${columnExpression(field.column, qualifier)} ${field.direction.toUpperCase()}`)
930
+ continue
931
+ }
932
+
933
+ const direction = field.direction === "desc" ? "DESC" : "ASC"
934
+ const path = jsonPath(field.propertyId)
935
+ clauses.push(
936
+ `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END ASC`,
937
+ `${jsonValueExpression(propertyColumn)} ${direction}`
938
+ )
939
+ args.push(path, path, path)
940
+ }
941
+
942
+ return { sql: clauses.join(", "), args, fields, propertyColumn }
943
+ }
944
+
945
+ function sortOrderFields(fields: readonly ObjectQuerySortField[]): readonly CompiledOrderField[] {
946
+ const orderFields: CompiledOrderField[] = []
947
+ for (const field of fields) {
948
+ if (field.kind !== "property") {
949
+ throw new Error("[Sixb] SQLite object storage does not support relevance sorting")
950
+ }
951
+ if (field.scalarKind === "decimal") {
952
+ throw new ObjectQueryExecutionError(
953
+ "exact_decimal_not_supported",
954
+ "SQLite object storage cannot push down exact decimal sorting",
955
+ "$.sort"
956
+ )
957
+ }
958
+
959
+ orderFields.push({
960
+ kind: "property",
961
+ propertyId: field.propertyId,
962
+ direction: field.direction === "desc" ? "desc" : "asc",
963
+ })
964
+ }
965
+ orderFields.push(...identityOrderFields())
966
+ return orderFields
967
+ }
968
+
969
+ function identityOrderFields(): readonly CompiledOrderField[] {
970
+ return [
971
+ { kind: "column", column: "object_type_id", direction: "asc" },
972
+ { kind: "column", column: "primary_id", direction: "asc" },
973
+ ]
974
+ }
975
+
976
+ function compileKeysetPredicate(
977
+ fields: readonly CompiledOrderField[],
978
+ cursor: readonly EncodedCursorValue[],
979
+ propertyColumn = "properties"
980
+ ): CompiledPredicate {
981
+ const disjuncts: CompiledPredicate[] = []
982
+
983
+ for (let index = 0; index < fields.length; index += 1) {
984
+ const equalityPredicates = fields
985
+ .slice(0, index)
986
+ .map((field, equalityIndex) =>
987
+ compileFieldEquality(field, cursor[equalityIndex], propertyColumn)
988
+ )
989
+ const advancePredicate = compileFieldAfter(fields[index], cursor[index], propertyColumn)
990
+ if (!advancePredicate) continue
991
+
992
+ const parts = [...equalityPredicates, advancePredicate]
993
+ disjuncts.push({
994
+ sql: `(${parts.map((part) => part.sql).join(" AND ")})`,
995
+ args: parts.flatMap((part) => part.args),
996
+ })
997
+ }
998
+
999
+ if (disjuncts.length === 0) return { sql: "0 = 1", args: [] }
1000
+
1001
+ return {
1002
+ sql: `(${disjuncts.map((part) => part.sql).join(" OR ")})`,
1003
+ args: disjuncts.flatMap((part) => part.args),
1004
+ }
1005
+ }
1006
+
1007
+ function compileFieldEquality(
1008
+ field: CompiledOrderField,
1009
+ cursor: EncodedCursorValue,
1010
+ propertyColumn = "properties"
1011
+ ): CompiledPredicate {
1012
+ if (field.kind === "column") {
1013
+ return {
1014
+ sql: `${columnExpression(field.column)} = ?`,
1015
+ args: [sqlValue(cursor.value)],
1016
+ }
1017
+ }
1018
+
1019
+ const path = jsonPath(field.propertyId)
1020
+ if (cursor.nullish) {
1021
+ return {
1022
+ sql: `(${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null')`,
1023
+ args: [path, path],
1024
+ }
1025
+ }
1026
+
1027
+ return {
1028
+ sql: `(${jsonTypeExpression(propertyColumn)} IS NOT NULL AND ${jsonTypeExpression(propertyColumn)} != 'null' AND ${jsonValueExpression(propertyColumn)} = ?)`,
1029
+ args: [path, path, path, sqlValue(cursor.value)],
1030
+ }
1031
+ }
1032
+
1033
+ function compileFieldAfter(
1034
+ field: CompiledOrderField,
1035
+ cursor: EncodedCursorValue,
1036
+ propertyColumn = "properties"
1037
+ ): CompiledPredicate | null {
1038
+ if (field.kind === "column") {
1039
+ return {
1040
+ sql: `${columnExpression(field.column)} ${field.direction === "desc" ? "<" : ">"} ?`,
1041
+ args: [sqlValue(cursor.value)],
1042
+ }
1043
+ }
1044
+
1045
+ if (cursor.nullish) return null
1046
+
1047
+ const path = jsonPath(field.propertyId)
1048
+ const rankSql = `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
1049
+ const valueOperator = field.direction === "desc" ? "<" : ">"
1050
+
1051
+ return {
1052
+ sql: `(${rankSql} > 0 OR (${rankSql} = 0 AND ${jsonValueExpression(propertyColumn)} ${valueOperator} ?))`,
1053
+ args: [path, path, path, path, path, sqlValue(cursor.value)],
1054
+ }
1055
+ }
1056
+
1057
+ function encodePageToken(
1058
+ row: SqliteObjectQueryPageRow,
1059
+ fields: readonly CompiledOrderField[]
1060
+ ): string {
1061
+ const properties = JSON.parse(row._cursor_properties ?? row.properties) as Record<string, unknown>
1062
+ const token: EncodedPageToken = {
1063
+ version: 1,
1064
+ order: fields.map(orderFieldKey),
1065
+ values: fields.map((field) => cursorValueForField(row, properties, field)),
1066
+ }
1067
+ return `${PAGE_TOKEN_PREFIX}${Buffer.from(JSON.stringify(token)).toString("base64url")}`
1068
+ }
1069
+
1070
+ function decodePageToken(
1071
+ token: string,
1072
+ fields: readonly CompiledOrderField[]
1073
+ ): readonly EncodedCursorValue[] {
1074
+ if (!token.startsWith(PAGE_TOKEN_PREFIX)) {
1075
+ throwInvalidPageToken("Invalid SQLite object query page token")
1076
+ }
1077
+
1078
+ const raw = token.slice(PAGE_TOKEN_PREFIX.length)
1079
+ let decoded: unknown
1080
+ try {
1081
+ decoded = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"))
1082
+ } catch {
1083
+ throwInvalidPageToken("Invalid SQLite object query page token")
1084
+ }
1085
+
1086
+ if (!isEncodedPageToken(decoded)) {
1087
+ throwInvalidPageToken("Invalid SQLite object query page token")
1088
+ }
1089
+
1090
+ const expectedOrder = fields.map(orderFieldKey)
1091
+ if (
1092
+ decoded.order.length !== expectedOrder.length ||
1093
+ decoded.order.some((field, index) => field !== expectedOrder[index])
1094
+ ) {
1095
+ throwInvalidPageToken("SQLite object query page token does not match query order")
1096
+ }
1097
+
1098
+ return decoded.values
1099
+ }
1100
+
1101
+ function throwInvalidPageToken(message: string): never {
1102
+ throw new ObjectQueryExecutionError("invalid_page_token", message, "$.pageToken")
1103
+ }
1104
+
1105
+ function isEncodedPageToken(value: unknown): value is EncodedPageToken {
1106
+ if (!isPlainObject(value)) return false
1107
+ if (value.version !== 1) return false
1108
+ if (!Array.isArray(value.order) || value.order.some((item) => typeof item !== "string")) {
1109
+ return false
1110
+ }
1111
+ if (!Array.isArray(value.values)) return false
1112
+ return value.values.every(
1113
+ (item) =>
1114
+ isPlainObject(item) &&
1115
+ typeof item.nullish === "boolean" &&
1116
+ (item.nullish || Object.hasOwn(item, "value"))
1117
+ )
1118
+ }
1119
+
1120
+ function cursorValueForField(
1121
+ row: SqliteObjectQueryPageRow,
1122
+ properties: Record<string, unknown>,
1123
+ field: CompiledOrderField
1124
+ ): EncodedCursorValue {
1125
+ const value =
1126
+ field.kind === "column"
1127
+ ? field.column === "object_type_id"
1128
+ ? row.object_type_id
1129
+ : row.primary_id
1130
+ : properties[field.propertyId]
1131
+
1132
+ return value === null || value === undefined ? { nullish: true } : { nullish: false, value }
1133
+ }
1134
+
1135
+ function orderFieldKey(field: CompiledOrderField): string {
1136
+ return field.kind === "column"
1137
+ ? `column:${field.column}:${field.direction}`
1138
+ : `property:${field.propertyId}:${field.direction}`
1139
+ }
1140
+
1141
+ function columnExpression(column: "object_type_id" | "primary_id", qualifier?: string): string {
1142
+ return qualifier ? `${qualifier}.${column}` : column
1143
+ }
1144
+
1145
+ function jsonValueExpression(column = "properties"): string {
1146
+ return `json_extract(${column}, ?)`
1147
+ }
1148
+
1149
+ function jsonTypeExpression(column = "properties"): string {
1150
+ return `json_type(${column}, ?)`
1151
+ }
1152
+
1153
+ function jsonPath(propertyId: string): string {
1154
+ return `$."${propertyId.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
1155
+ }
1156
+
1157
+ function sqlValue(value: unknown): SqliteValue {
1158
+ if (value instanceof Date) return value.toISOString()
1159
+ if (typeof value === "boolean") return value ? 1 : 0
1160
+ if (
1161
+ typeof value === "string" ||
1162
+ typeof value === "number" ||
1163
+ typeof value === "bigint" ||
1164
+ value === null
1165
+ ) {
1166
+ return value
1167
+ }
1168
+ return JSON.stringify(value)
1169
+ }
1170
+
1171
+ function tokenize(query: string): string[] {
1172
+ return query
1173
+ .trim()
1174
+ .toLowerCase()
1175
+ .split(/\s+/)
1176
+ .filter((term) => term.length > 0)
1177
+ }
1178
+
1179
+ function sqlComparisonOperator(op: "lt" | "lte" | "gt" | "gte"): "<" | "<=" | ">" | ">=" {
1180
+ switch (op) {
1181
+ case "lt":
1182
+ return "<"
1183
+ case "lte":
1184
+ return "<="
1185
+ case "gt":
1186
+ return ">"
1187
+ case "gte":
1188
+ return ">="
1189
+ }
1190
+ }
1191
+
1192
+ function identityRows<T>(rows: readonly T[]): readonly T[] {
1193
+ return rows
1194
+ }
1195
+
1196
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
1197
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1198
+ }