@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.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/action-run-storage.d.ts +29 -0
- package/dist/action-run-storage.d.ts.map +1 -0
- package/dist/agents/agent-storage.d.ts +25 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +14 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +26 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +14 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +46 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +28 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +134 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +131 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10204 -0
- package/dist/index.js.map +54 -0
- package/dist/latest-run-query.d.ts +22 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +18 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/object-query-compiler.d.ts +44 -0
- package/dist/object-query-compiler.d.ts.map +1 -0
- package/dist/object-storage.d.ts +93 -0
- package/dist/object-storage.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +14 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +49 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +63 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +32 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +14 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +91 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +26 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts +26 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -0
- package/dist/projection-run-storage.d.ts +32 -0
- package/dist/projection-run-storage.d.ts.map +1 -0
- package/dist/rules-storage.d.ts +33 -0
- package/dist/rules-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +27 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/sync-run-storage.d.ts +23 -0
- package/dist/sync-run-storage.d.ts.map +1 -0
- package/dist/timeseries-storage.d.ts +41 -0
- package/dist/timeseries-storage.d.ts.map +1 -0
- package/dist/transactions.d.ts +15 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/webhook-delivery-storage.d.ts +27 -0
- package/dist/webhook-delivery-storage.d.ts.map +1 -0
- package/dist/webhook-run-storage.d.ts +22 -0
- package/dist/webhook-run-storage.d.ts.map +1 -0
- package/dist/workflow-intervention-storage.d.ts +26 -0
- package/dist/workflow-intervention-storage.d.ts.map +1 -0
- package/dist/workflow-run-storage.d.ts +63 -0
- package/dist/workflow-run-storage.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/action-run-storage.ts +761 -0
- package/src/agents/agent-storage.ts +46 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +162 -0
- package/src/agents/rows.ts +205 -0
- package/src/agents/runs.ts +389 -0
- package/src/agents/threads.ts +133 -0
- package/src/auth-storage/access-tokens.ts +289 -0
- package/src/auth-storage/auth-storage.ts +784 -0
- package/src/auth-storage/group-memberships.ts +52 -0
- package/src/auth-storage/identities.ts +98 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +258 -0
- package/src/auth-storage/magic-links.ts +149 -0
- package/src/auth-storage/oidc-attempts.ts +109 -0
- package/src/auth-storage/rows.ts +338 -0
- package/src/auth-storage/service-account-group-memberships.ts +161 -0
- package/src/auth-storage/service-accounts.ts +168 -0
- package/src/auth-storage/sessions.ts +230 -0
- package/src/auth-storage/shared.ts +707 -0
- package/src/auth-storage/users.ts +226 -0
- package/src/index.ts +292 -0
- package/src/latest-run-query.ts +49 -0
- package/src/migrations/001-initial-schema.sql +1068 -0
- package/src/migrations.ts +245 -0
- package/src/object-query-compiler.ts +1198 -0
- package/src/object-storage.ts +605 -0
- package/src/ontology-storage/commits.ts +127 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +393 -0
- package/src/ontology-storage/materialization-state.ts +901 -0
- package/src/ontology-storage/materialization-writer.ts +394 -0
- package/src/ontology-storage/materializations.ts +838 -0
- package/src/ontology-storage/outbox.ts +237 -0
- package/src/ontology-storage/shared.ts +269 -0
- package/src/ontology-storage/sources.ts +653 -0
- package/src/pipeline-run-storage.ts +506 -0
- package/src/projection-run-storage.ts +592 -0
- package/src/rules-storage.ts +375 -0
- package/src/run-list-query.ts +98 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +3 -0
- package/src/sync-run-storage.ts +299 -0
- package/src/timeseries-storage.ts +212 -0
- package/src/transactions.ts +96 -0
- package/src/validation.ts +30 -0
- package/src/webhook-delivery-storage.ts +272 -0
- package/src/webhook-run-storage.ts +240 -0
- package/src/workflow-intervention-storage.ts +398 -0
- 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
|
+
}
|