@sixb/pg 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 +73 -0
- package/dist/agents/agent-storage.d.ts +20 -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 +13 -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 +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -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 +41 -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 +29 -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 +138 -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 +159 -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 +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -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 +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -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 +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- package/src/transactions.ts +58 -0
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ObjectExpansion,
|
|
3
|
+
type ObjectQuery,
|
|
4
|
+
ObjectQueryExecutionError,
|
|
5
|
+
type ObjectQueryPredicate,
|
|
6
|
+
type ObjectQuerySetOperation,
|
|
7
|
+
type ObjectQuerySortField,
|
|
8
|
+
type QueryScalarKind,
|
|
9
|
+
} from "@sixb/core"
|
|
10
|
+
|
|
11
|
+
export interface PgObjectQueryPageRow {
|
|
12
|
+
object_type_id: string
|
|
13
|
+
primary_id: string
|
|
14
|
+
properties: unknown
|
|
15
|
+
_cursor_properties?: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CompiledPgObjectQuery {
|
|
19
|
+
sql: string
|
|
20
|
+
args: unknown[]
|
|
21
|
+
totalSql: string
|
|
22
|
+
totalArgs: unknown[]
|
|
23
|
+
order: CompiledOrder
|
|
24
|
+
hasMoreProbe?: CompiledHasMoreProbe
|
|
25
|
+
hasMore(rowCount: number, total?: number): boolean
|
|
26
|
+
trimRows<T>(rows: readonly T[]): readonly T[]
|
|
27
|
+
nextPageToken(rows: readonly PgObjectQueryPageRow[], rowCount: number): string | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompiledPgScalarQuery {
|
|
31
|
+
sql: string
|
|
32
|
+
args: unknown[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CompiledPgFacetQuery {
|
|
36
|
+
sql: string
|
|
37
|
+
args: unknown[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CompiledHasMoreProbe {
|
|
41
|
+
sql: string
|
|
42
|
+
args: unknown[]
|
|
43
|
+
hasMore(rowCount: number): boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface CompileContext {
|
|
47
|
+
probeLimit: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CompiledPredicate {
|
|
51
|
+
sql: string
|
|
52
|
+
args: unknown[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CompiledAggregateSource {
|
|
56
|
+
sql: string
|
|
57
|
+
args: unknown[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CompiledOrder {
|
|
61
|
+
sql: string
|
|
62
|
+
args: unknown[]
|
|
63
|
+
fields: readonly CompiledOrderField[]
|
|
64
|
+
propertyColumn: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type CompiledOrderField =
|
|
68
|
+
| {
|
|
69
|
+
kind: "property"
|
|
70
|
+
propertyId: string
|
|
71
|
+
direction: "asc" | "desc"
|
|
72
|
+
scalarKind?: QueryScalarKind
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
kind: "column"
|
|
76
|
+
column: "object_type_id" | "primary_id"
|
|
77
|
+
direction: "asc" | "desc"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface EncodedPageToken {
|
|
81
|
+
version: 1
|
|
82
|
+
order: readonly string[]
|
|
83
|
+
values: readonly EncodedCursorValue[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface EncodedCursorValue {
|
|
87
|
+
nullish: boolean
|
|
88
|
+
value?: unknown
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const PAGE_TOKEN_PREFIX = "keyset:"
|
|
92
|
+
const exactContext: CompileContext = { probeLimit: false }
|
|
93
|
+
// Per-parent fanout the core executor bakes into every pushed-down expansion;
|
|
94
|
+
// this default only guards a malformed (un-baked) IR and mirrors the core cap.
|
|
95
|
+
const DEFAULT_EXPANSION_FANOUT = 1_000
|
|
96
|
+
|
|
97
|
+
export function compilePgObjectQuery(
|
|
98
|
+
projectId: string,
|
|
99
|
+
query: ObjectQuery,
|
|
100
|
+
options: { includeTotal?: boolean } = {}
|
|
101
|
+
): CompiledPgObjectQuery {
|
|
102
|
+
const compiled = compileObjectQueryInternal(projectId, query, {
|
|
103
|
+
probeLimit: options.includeTotal === false,
|
|
104
|
+
})
|
|
105
|
+
return {
|
|
106
|
+
...compiled,
|
|
107
|
+
sql: numberPlaceholders(compiled.sql),
|
|
108
|
+
totalSql: numberPlaceholders(compiled.totalSql),
|
|
109
|
+
hasMoreProbe: compiled.hasMoreProbe
|
|
110
|
+
? {
|
|
111
|
+
...compiled.hasMoreProbe,
|
|
112
|
+
sql: numberPlaceholders(compiled.hasMoreProbe.sql),
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function compilePgObjectCountQuery(
|
|
119
|
+
projectId: string,
|
|
120
|
+
query: ObjectQuery
|
|
121
|
+
): CompiledPgScalarQuery {
|
|
122
|
+
const source = compileAggregateSource(projectId, query)
|
|
123
|
+
return numberCompiledQuery({
|
|
124
|
+
sql: `
|
|
125
|
+
SELECT COUNT(*)::bigint AS count
|
|
126
|
+
FROM (${source.sql}) AS input
|
|
127
|
+
`,
|
|
128
|
+
args: source.args,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function compilePgObjectExistsQuery(
|
|
133
|
+
projectId: string,
|
|
134
|
+
query: ObjectQuery
|
|
135
|
+
): CompiledPgScalarQuery {
|
|
136
|
+
const source = compileAggregateSource(projectId, query)
|
|
137
|
+
return numberCompiledQuery({
|
|
138
|
+
sql: `
|
|
139
|
+
SELECT 1
|
|
140
|
+
FROM (${source.sql}) AS input
|
|
141
|
+
LIMIT 1
|
|
142
|
+
`,
|
|
143
|
+
args: source.args,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function compilePgObjectFacetQuery(
|
|
148
|
+
projectId: string,
|
|
149
|
+
query: ObjectQuery,
|
|
150
|
+
propertyId: string,
|
|
151
|
+
limit: number
|
|
152
|
+
): CompiledPgFacetQuery {
|
|
153
|
+
const source = compileAggregateSource(projectId, query)
|
|
154
|
+
return numberCompiledQuery({
|
|
155
|
+
sql: `
|
|
156
|
+
SELECT facet.value_type, facet.value_text, COUNT(*)::bigint AS count
|
|
157
|
+
FROM (
|
|
158
|
+
SELECT
|
|
159
|
+
jsonb_typeof(input.properties -> (?::text)) AS value_type,
|
|
160
|
+
input.properties ->> (?::text) AS value_text
|
|
161
|
+
FROM (${source.sql}) AS input
|
|
162
|
+
WHERE jsonb_exists(input.properties, ?::text)
|
|
163
|
+
) AS facet
|
|
164
|
+
GROUP BY facet.value_type, facet.value_text
|
|
165
|
+
ORDER BY count DESC, facet.value_text ASC
|
|
166
|
+
LIMIT ?
|
|
167
|
+
`,
|
|
168
|
+
args: [propertyId, propertyId, ...source.args, propertyId, limit],
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function compileObjectQueryInternal(
|
|
173
|
+
projectId: string,
|
|
174
|
+
query: ObjectQuery,
|
|
175
|
+
ctx: CompileContext
|
|
176
|
+
): CompiledPgObjectQuery {
|
|
177
|
+
switch (query.kind) {
|
|
178
|
+
case "start":
|
|
179
|
+
return compileStart(projectId, query)
|
|
180
|
+
case "filter":
|
|
181
|
+
return compileFilter(projectId, query.input, query.predicate)
|
|
182
|
+
case "sort":
|
|
183
|
+
return compileSort(projectId, query.input, query.fields, ctx)
|
|
184
|
+
case "limit":
|
|
185
|
+
return compileLimit(projectId, query.input, query.limit, ctx)
|
|
186
|
+
case "page":
|
|
187
|
+
return compilePage(projectId, query.input, query.pageSize, query.pageToken)
|
|
188
|
+
case "traverse":
|
|
189
|
+
return compileTraversal(
|
|
190
|
+
projectId,
|
|
191
|
+
query.input,
|
|
192
|
+
query.linkId,
|
|
193
|
+
query.direction,
|
|
194
|
+
query.sourceObjectTypeId
|
|
195
|
+
)
|
|
196
|
+
case "set":
|
|
197
|
+
return compileSet(projectId, query.op, query.inputs)
|
|
198
|
+
case "project":
|
|
199
|
+
return compileProject(projectId, query.input, query.properties, ctx)
|
|
200
|
+
case "text":
|
|
201
|
+
return compileText(
|
|
202
|
+
projectId,
|
|
203
|
+
query.input,
|
|
204
|
+
query.query,
|
|
205
|
+
query.fields,
|
|
206
|
+
query.fieldsByObjectType
|
|
207
|
+
)
|
|
208
|
+
case "expand":
|
|
209
|
+
return compileExpand(projectId, query.input, query.expansions)
|
|
210
|
+
case "vector":
|
|
211
|
+
throw new Error(
|
|
212
|
+
`[SixbPg] PostgreSQL object storage does not support query node '${query.kind}'`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function compileStart(
|
|
218
|
+
projectId: string,
|
|
219
|
+
query: Extract<ObjectQuery, { kind: "start" }>
|
|
220
|
+
): CompiledPgObjectQuery {
|
|
221
|
+
if (query.includeSubtypes === true) {
|
|
222
|
+
throw new Error("[SixbPg] PostgreSQL object storage does not support start.includeSubtypes")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const order = compileOrder(identityOrderFields())
|
|
226
|
+
const sql = `
|
|
227
|
+
SELECT *, properties AS _cursor_properties
|
|
228
|
+
FROM objects
|
|
229
|
+
WHERE project_id = ? AND object_type_id = ?
|
|
230
|
+
ORDER BY ${order.sql}
|
|
231
|
+
`
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
sql,
|
|
235
|
+
args: [projectId, query.objectTypeId, ...order.args],
|
|
236
|
+
totalSql:
|
|
237
|
+
"SELECT COUNT(*)::bigint AS total FROM objects WHERE project_id = ? AND object_type_id = ?",
|
|
238
|
+
totalArgs: [projectId, query.objectTypeId],
|
|
239
|
+
order,
|
|
240
|
+
hasMore: () => false,
|
|
241
|
+
trimRows: identityRows,
|
|
242
|
+
nextPageToken: () => undefined,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function compileFilter(
|
|
247
|
+
projectId: string,
|
|
248
|
+
inputQuery: ObjectQuery,
|
|
249
|
+
predicateNode: ObjectQueryPredicate
|
|
250
|
+
): CompiledPgObjectQuery {
|
|
251
|
+
return compileWhere(projectId, inputQuery, compilePredicate(predicateNode))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function compileWhere(
|
|
255
|
+
projectId: string,
|
|
256
|
+
inputQuery: ObjectQuery,
|
|
257
|
+
predicate: CompiledPredicate
|
|
258
|
+
): CompiledPgObjectQuery {
|
|
259
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
|
|
260
|
+
const sql = `
|
|
261
|
+
SELECT *
|
|
262
|
+
FROM (${input.sql}) AS input
|
|
263
|
+
WHERE ${predicate.sql}
|
|
264
|
+
ORDER BY ${input.order.sql}
|
|
265
|
+
`
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
sql,
|
|
269
|
+
args: [...input.args, ...predicate.args, ...input.order.args],
|
|
270
|
+
totalSql: `
|
|
271
|
+
SELECT COUNT(*)::bigint AS total
|
|
272
|
+
FROM (${input.sql}) AS input
|
|
273
|
+
WHERE ${predicate.sql}
|
|
274
|
+
`,
|
|
275
|
+
totalArgs: [...input.args, ...predicate.args],
|
|
276
|
+
order: input.order,
|
|
277
|
+
hasMore: () => false,
|
|
278
|
+
trimRows: identityRows,
|
|
279
|
+
nextPageToken: () => undefined,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function compileText(
|
|
284
|
+
projectId: string,
|
|
285
|
+
inputQuery: ObjectQuery,
|
|
286
|
+
query: string,
|
|
287
|
+
fields: readonly string[] | undefined,
|
|
288
|
+
fieldsByObjectType: Readonly<Record<string, readonly string[]>> | undefined
|
|
289
|
+
): CompiledPgObjectQuery {
|
|
290
|
+
return compileWhere(
|
|
291
|
+
projectId,
|
|
292
|
+
inputQuery,
|
|
293
|
+
compileTextSearchPredicate(query, fields, fieldsByObjectType)
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function compileSort(
|
|
298
|
+
projectId: string,
|
|
299
|
+
inputQuery: ObjectQuery,
|
|
300
|
+
fields: readonly ObjectQuerySortField[],
|
|
301
|
+
ctx: CompileContext
|
|
302
|
+
): CompiledPgObjectQuery {
|
|
303
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
|
|
304
|
+
const probeInput =
|
|
305
|
+
ctx.probeLimit && needsLimitProbe(inputQuery)
|
|
306
|
+
? compileObjectQueryInternal(projectId, inputQuery, ctx)
|
|
307
|
+
: undefined
|
|
308
|
+
const order = compileOrder(sortOrderFields(fields))
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
sql: `
|
|
312
|
+
SELECT
|
|
313
|
+
input.project_id,
|
|
314
|
+
input.object_type_id,
|
|
315
|
+
input.primary_id,
|
|
316
|
+
input.properties,
|
|
317
|
+
input.properties AS _cursor_properties,
|
|
318
|
+
input.created_at,
|
|
319
|
+
input.updated_at,
|
|
320
|
+
input.version,
|
|
321
|
+
input.last_commit_id
|
|
322
|
+
FROM (${input.sql}) AS input
|
|
323
|
+
ORDER BY ${order.sql}
|
|
324
|
+
`,
|
|
325
|
+
args: [...input.args, ...order.args],
|
|
326
|
+
totalSql: input.totalSql,
|
|
327
|
+
totalArgs: input.totalArgs,
|
|
328
|
+
order,
|
|
329
|
+
hasMoreProbe: probeInput ? hasMoreProbeFor(probeInput) : input.hasMoreProbe,
|
|
330
|
+
hasMore: input.hasMore,
|
|
331
|
+
trimRows: input.trimRows,
|
|
332
|
+
nextPageToken: input.nextPageToken,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function compileLimit(
|
|
337
|
+
projectId: string,
|
|
338
|
+
inputQuery: ObjectQuery,
|
|
339
|
+
rawLimit: number,
|
|
340
|
+
ctx: CompileContext
|
|
341
|
+
): CompiledPgObjectQuery {
|
|
342
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
|
|
343
|
+
const limit = Math.max(0, rawLimit)
|
|
344
|
+
const rowLimit = ctx.probeLimit ? limit + 1 : limit
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
sql: `
|
|
348
|
+
SELECT *
|
|
349
|
+
FROM (${input.sql}) AS input
|
|
350
|
+
ORDER BY ${input.order.sql}
|
|
351
|
+
LIMIT ?
|
|
352
|
+
`,
|
|
353
|
+
args: [...input.args, ...input.order.args, rowLimit],
|
|
354
|
+
totalSql: `
|
|
355
|
+
SELECT COUNT(*)::bigint AS total
|
|
356
|
+
FROM (${input.sql}) AS input
|
|
357
|
+
`,
|
|
358
|
+
totalArgs: input.args,
|
|
359
|
+
order: input.order,
|
|
360
|
+
hasMore: (rowCount, total) =>
|
|
361
|
+
total === undefined ? ctx.probeLimit && rowCount > limit : limit < total,
|
|
362
|
+
trimRows: ctx.probeLimit ? (rows) => rows.slice(0, limit) : identityRows,
|
|
363
|
+
nextPageToken: () => undefined,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function compilePage(
|
|
368
|
+
projectId: string,
|
|
369
|
+
inputQuery: ObjectQuery,
|
|
370
|
+
rawPageSize: number,
|
|
371
|
+
pageToken: string | undefined
|
|
372
|
+
): CompiledPgObjectQuery {
|
|
373
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
|
|
374
|
+
const pageSize = Math.max(0, rawPageSize)
|
|
375
|
+
const cursor = pageToken ? decodePageToken(pageToken, input.order.fields) : undefined
|
|
376
|
+
const cursorPredicate = cursor
|
|
377
|
+
? compileKeysetPredicate(input.order.fields, cursor, input.order.propertyColumn)
|
|
378
|
+
: undefined
|
|
379
|
+
const whereSql = cursorPredicate ? `WHERE ${cursorPredicate.sql}` : ""
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
sql: `
|
|
383
|
+
SELECT *
|
|
384
|
+
FROM (${input.sql}) AS input
|
|
385
|
+
${whereSql}
|
|
386
|
+
ORDER BY ${input.order.sql}
|
|
387
|
+
LIMIT ?
|
|
388
|
+
`,
|
|
389
|
+
args: [...input.args, ...(cursorPredicate?.args ?? []), ...input.order.args, pageSize + 1],
|
|
390
|
+
totalSql: `
|
|
391
|
+
SELECT COUNT(*)::bigint AS total
|
|
392
|
+
FROM (${input.sql}) AS input
|
|
393
|
+
`,
|
|
394
|
+
totalArgs: input.args,
|
|
395
|
+
order: input.order,
|
|
396
|
+
hasMore: (rowCount) => rowCount > pageSize,
|
|
397
|
+
trimRows: (rows) => rows.slice(0, pageSize),
|
|
398
|
+
nextPageToken: (rows, rowCount) => {
|
|
399
|
+
if (rowCount <= pageSize || rows.length === 0) return undefined
|
|
400
|
+
return encodePageToken(rows[rows.length - 1], input.order.fields)
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function compileTraversal(
|
|
406
|
+
projectId: string,
|
|
407
|
+
inputQuery: ObjectQuery,
|
|
408
|
+
linkId: string,
|
|
409
|
+
direction: "outgoing" | "incoming",
|
|
410
|
+
sourceObjectTypeId?: string
|
|
411
|
+
): CompiledPgObjectQuery {
|
|
412
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
|
|
413
|
+
const outputAlias = direction === "outgoing" ? "target_object" : "source_object"
|
|
414
|
+
const joinSql =
|
|
415
|
+
direction === "outgoing"
|
|
416
|
+
? `
|
|
417
|
+
JOIN links AS edge
|
|
418
|
+
ON edge.project_id = input.project_id
|
|
419
|
+
AND edge.source_type_id = input.object_type_id
|
|
420
|
+
AND edge.source_id = input.primary_id
|
|
421
|
+
AND edge.link_id = ?
|
|
422
|
+
JOIN objects AS target_object
|
|
423
|
+
ON target_object.project_id = edge.project_id
|
|
424
|
+
AND target_object.object_type_id = edge.target_type_id
|
|
425
|
+
AND target_object.primary_id = edge.target_id
|
|
426
|
+
`
|
|
427
|
+
: `
|
|
428
|
+
JOIN links AS edge
|
|
429
|
+
ON edge.project_id = input.project_id
|
|
430
|
+
AND edge.target_type_id = input.object_type_id
|
|
431
|
+
AND edge.target_id = input.primary_id
|
|
432
|
+
AND edge.link_id = ?${sourceObjectTypeId === undefined ? "" : "\n AND edge.source_type_id = ?"}
|
|
433
|
+
JOIN objects AS source_object
|
|
434
|
+
ON source_object.project_id = edge.project_id
|
|
435
|
+
AND source_object.object_type_id = edge.source_type_id
|
|
436
|
+
AND source_object.primary_id = edge.source_id
|
|
437
|
+
`
|
|
438
|
+
const order = compileOrder(identityOrderFields())
|
|
439
|
+
const qualifiedOrder = compileOrder(identityOrderFields(), outputAlias)
|
|
440
|
+
const sql = `
|
|
441
|
+
SELECT DISTINCT ${outputAlias}.*, ${outputAlias}.properties AS _cursor_properties
|
|
442
|
+
FROM (${input.sql}) AS input
|
|
443
|
+
${joinSql}
|
|
444
|
+
ORDER BY ${qualifiedOrder.sql}
|
|
445
|
+
`
|
|
446
|
+
const args = [
|
|
447
|
+
...input.args,
|
|
448
|
+
linkId,
|
|
449
|
+
...(sourceObjectTypeId === undefined ? [] : [sourceObjectTypeId]),
|
|
450
|
+
...qualifiedOrder.args,
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
sql,
|
|
455
|
+
args,
|
|
456
|
+
totalSql: `SELECT COUNT(*)::bigint AS total FROM (${sql}) AS traversed`,
|
|
457
|
+
totalArgs: args,
|
|
458
|
+
order,
|
|
459
|
+
hasMore: () => false,
|
|
460
|
+
trimRows: identityRows,
|
|
461
|
+
nextPageToken: () => undefined,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Correlation handle for an expansion: the parent row's identity columns. */
|
|
466
|
+
interface ExpansionParent {
|
|
467
|
+
project: string
|
|
468
|
+
type: string
|
|
469
|
+
id: string
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// `expand` attaches linked objects to each result row without changing which
|
|
473
|
+
// objects match (unlike `traverse`, which pivots to the targets). Each expansion
|
|
474
|
+
// becomes a correlated subquery that hydrates its links into a JSONB value, and
|
|
475
|
+
// all expansions are folded into one `_expand` object column the row mapper reads
|
|
476
|
+
// back. The input set is otherwise untouched, so pagination/order pass through.
|
|
477
|
+
function compileExpand(
|
|
478
|
+
projectId: string,
|
|
479
|
+
inputQuery: ObjectQuery,
|
|
480
|
+
expansions: readonly ObjectExpansion[]
|
|
481
|
+
): CompiledPgObjectQuery {
|
|
482
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, exactContext)
|
|
483
|
+
const expand = compileExpansionsObject(
|
|
484
|
+
expansions,
|
|
485
|
+
{ project: "input.project_id", type: "input.object_type_id", id: "input.primary_id" },
|
|
486
|
+
""
|
|
487
|
+
)
|
|
488
|
+
const inputOrder = compileOrder(input.order.fields, "input", "input._cursor_properties")
|
|
489
|
+
const sql = `
|
|
490
|
+
SELECT input.*, ${expand.sql} AS _expand
|
|
491
|
+
FROM (${input.sql}) AS input
|
|
492
|
+
ORDER BY ${inputOrder.sql}
|
|
493
|
+
`
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
sql,
|
|
497
|
+
args: [...expand.args, ...input.args, ...inputOrder.args],
|
|
498
|
+
totalSql: input.totalSql,
|
|
499
|
+
totalArgs: input.totalArgs,
|
|
500
|
+
order: input.order,
|
|
501
|
+
hasMoreProbe: input.hasMoreProbe,
|
|
502
|
+
hasMore: input.hasMore,
|
|
503
|
+
trimRows: input.trimRows,
|
|
504
|
+
nextPageToken: input.nextPageToken,
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// `jsonb_build_object(linkId, value, ...)` over a set of expansions sharing one
|
|
509
|
+
// parent. Link ids are user data, so they ride as parameters. Used both for the
|
|
510
|
+
// outer `_expand` column and for each child's nested `links`.
|
|
511
|
+
function compileExpansionsObject(
|
|
512
|
+
expansions: readonly ObjectExpansion[],
|
|
513
|
+
parent: ExpansionParent,
|
|
514
|
+
pathPrefix: string
|
|
515
|
+
): CompiledPredicate {
|
|
516
|
+
const parts: string[] = []
|
|
517
|
+
const args: unknown[] = []
|
|
518
|
+
expansions.forEach((expansion, index) => {
|
|
519
|
+
const path = pathPrefix === "" ? `${index}` : `${pathPrefix}_${index}`
|
|
520
|
+
const value = compileExpansionValue(expansion, parent, path)
|
|
521
|
+
parts.push("?::text", value.sql)
|
|
522
|
+
args.push(expansion.linkId, ...value.args)
|
|
523
|
+
})
|
|
524
|
+
return { sql: `jsonb_build_object(${parts.join(", ")})`, args }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// One expansion's hydrated value. `row_number()` numbers a parent's links by the
|
|
528
|
+
// order from `compileExpansionOrder` (the expansion's `orderBy` against the target
|
|
529
|
+
// object's properties, then an identity tiebreak); `WHERE _ord <= N` keeps the
|
|
530
|
+
// top-N per parent in-DB; each retained neighbour becomes a JSONB object; and the
|
|
531
|
+
// value is an ordered array ("many") or the first element or null ("one"). The
|
|
532
|
+
// cardinality is core-resolved before pushdown.
|
|
533
|
+
function compileExpansionValue(
|
|
534
|
+
expansion: ObjectExpansion,
|
|
535
|
+
parent: ExpansionParent,
|
|
536
|
+
path: string
|
|
537
|
+
): CompiledPredicate {
|
|
538
|
+
const edge = `edge_${path}`
|
|
539
|
+
const target = `tgt_${path}`
|
|
540
|
+
const ranked = `ranked_${path}`
|
|
541
|
+
const incoming = expansion.direction === "incoming"
|
|
542
|
+
// The hydrated neighbour is the link target (outgoing) or source (incoming);
|
|
543
|
+
// the parent sits on the opposite end, mirroring `compileTraversal`.
|
|
544
|
+
const neighborType = incoming ? `${edge}.source_type_id` : `${edge}.target_type_id`
|
|
545
|
+
const neighborId = incoming ? `${edge}.source_id` : `${edge}.target_id`
|
|
546
|
+
const parentType = incoming ? `${edge}.target_type_id` : `${edge}.source_type_id`
|
|
547
|
+
const parentId = incoming ? `${edge}.target_id` : `${edge}.source_id`
|
|
548
|
+
|
|
549
|
+
const child = compileExpansionChildJson(expansion, edge, target, path)
|
|
550
|
+
const order = compileExpansionOrder(expansion, target, neighborType, neighborId)
|
|
551
|
+
|
|
552
|
+
const whereParts = [
|
|
553
|
+
`${edge}.project_id = ${parent.project}`,
|
|
554
|
+
`${parentType} = ${parent.type}`,
|
|
555
|
+
`${parentId} = ${parent.id}`,
|
|
556
|
+
`${edge}.link_id = ?::text`,
|
|
557
|
+
]
|
|
558
|
+
const whereArgs: unknown[] = [expansion.linkId]
|
|
559
|
+
if (incoming && expansion.sourceObjectTypeId !== undefined) {
|
|
560
|
+
whereParts.push(`${edge}.source_type_id = ?::text`)
|
|
561
|
+
whereArgs.push(expansion.sourceObjectTypeId)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const inner = `
|
|
565
|
+
SELECT ${child.sql} AS elem, row_number() OVER (ORDER BY ${order.sql}) AS _ord
|
|
566
|
+
FROM links AS ${edge}
|
|
567
|
+
JOIN objects AS ${target}
|
|
568
|
+
ON ${target}.project_id = ${edge}.project_id
|
|
569
|
+
AND ${target}.object_type_id = ${neighborType}
|
|
570
|
+
AND ${target}.primary_id = ${neighborId}
|
|
571
|
+
WHERE ${whereParts.join(" AND ")}
|
|
572
|
+
`
|
|
573
|
+
const innerArgs = [...child.args, ...order.args, ...whereArgs]
|
|
574
|
+
|
|
575
|
+
if (expansion.cardinality === "one") {
|
|
576
|
+
return {
|
|
577
|
+
sql: `(SELECT ${ranked}.elem FROM (${inner}) AS ${ranked} WHERE ${ranked}._ord = 1)`,
|
|
578
|
+
args: innerArgs,
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// The fanout cap is baked into `limit` by the core executor before pushdown.
|
|
583
|
+
const limit = expansion.limit ?? DEFAULT_EXPANSION_FANOUT
|
|
584
|
+
return {
|
|
585
|
+
sql: `COALESCE((SELECT jsonb_agg(${ranked}.elem ORDER BY ${ranked}._ord) FROM (${inner}) AS ${ranked} WHERE ${ranked}._ord <= ?), '[]'::jsonb)`,
|
|
586
|
+
args: [...innerArgs, limit],
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Build one hydrated neighbour as an `ExpandedObjectRow`-shaped JSONB object.
|
|
591
|
+
// `linkProperties` always rides along (the mapper drops it when empty), and
|
|
592
|
+
// nested expansions recurse under `links`, correlated on this neighbour.
|
|
593
|
+
function compileExpansionChildJson(
|
|
594
|
+
expansion: ObjectExpansion,
|
|
595
|
+
edge: string,
|
|
596
|
+
target: string,
|
|
597
|
+
path: string
|
|
598
|
+
): CompiledPredicate {
|
|
599
|
+
const fields = [
|
|
600
|
+
`'projectId', ${target}.project_id`,
|
|
601
|
+
`'objectTypeId', ${target}.object_type_id`,
|
|
602
|
+
`'primaryId', ${target}.primary_id`,
|
|
603
|
+
`'properties', ${target}.properties`,
|
|
604
|
+
`'createdAt', ${target}.created_at`,
|
|
605
|
+
`'updatedAt', ${target}.updated_at`,
|
|
606
|
+
`'version', ${target}.version`,
|
|
607
|
+
`'lastCommitId', ${target}.last_commit_id`,
|
|
608
|
+
`'linkProperties', ${edge}.properties`,
|
|
609
|
+
]
|
|
610
|
+
const args: unknown[] = []
|
|
611
|
+
|
|
612
|
+
if (expansion.expand && expansion.expand.length > 0) {
|
|
613
|
+
const nested = compileExpansionsObject(
|
|
614
|
+
expansion.expand,
|
|
615
|
+
{
|
|
616
|
+
project: `${target}.project_id`,
|
|
617
|
+
type: `${target}.object_type_id`,
|
|
618
|
+
id: `${target}.primary_id`,
|
|
619
|
+
},
|
|
620
|
+
path
|
|
621
|
+
)
|
|
622
|
+
fields.push(`'links', ${nested.sql}`)
|
|
623
|
+
args.push(...nested.args)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { sql: `jsonb_build_object(${fields.join(", ")})`, args }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Order a parent's links the way the fallback's per-parent trim does: each
|
|
630
|
+
// property sorts nulls last with the requested direction (against the neighbour's
|
|
631
|
+
// JSONB properties), then a deterministic identity tiebreak on the neighbour.
|
|
632
|
+
function compileExpansionOrder(
|
|
633
|
+
expansion: ObjectExpansion,
|
|
634
|
+
target: string,
|
|
635
|
+
neighborType: string,
|
|
636
|
+
neighborId: string
|
|
637
|
+
): CompiledPredicate {
|
|
638
|
+
const propertyColumn = `${target}.properties`
|
|
639
|
+
const clauses: string[] = []
|
|
640
|
+
const args: unknown[] = []
|
|
641
|
+
|
|
642
|
+
for (const field of expansion.orderBy ?? []) {
|
|
643
|
+
// Relevance is a no-op in the fallback comparator; mirror that by skipping it.
|
|
644
|
+
if (field.kind !== "property") continue
|
|
645
|
+
const direction = field.direction === "desc" ? "DESC" : "ASC"
|
|
646
|
+
clauses.push(
|
|
647
|
+
`CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END ASC`,
|
|
648
|
+
`${
|
|
649
|
+
field.scalarKind === "decimal"
|
|
650
|
+
? `(${jsonTextExpression(propertyColumn)})::numeric`
|
|
651
|
+
: jsonValueExpression(propertyColumn)
|
|
652
|
+
} ${direction}`
|
|
653
|
+
)
|
|
654
|
+
args.push(field.propertyId, field.propertyId, field.propertyId)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
clauses.push(`${neighborType} ASC`, `${neighborId} ASC`)
|
|
658
|
+
return { sql: clauses.join(", "), args }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function compileSet(
|
|
662
|
+
projectId: string,
|
|
663
|
+
op: ObjectQuerySetOperation,
|
|
664
|
+
inputs: readonly ObjectQuery[]
|
|
665
|
+
): CompiledPgObjectQuery {
|
|
666
|
+
if (inputs.length === 0) {
|
|
667
|
+
const order = compileOrder(identityOrderFields())
|
|
668
|
+
return {
|
|
669
|
+
sql: `
|
|
670
|
+
SELECT *, properties AS _cursor_properties
|
|
671
|
+
FROM objects
|
|
672
|
+
WHERE 1 = 0
|
|
673
|
+
ORDER BY ${order.sql}
|
|
674
|
+
`,
|
|
675
|
+
args: order.args,
|
|
676
|
+
totalSql: "SELECT 0::bigint AS total",
|
|
677
|
+
totalArgs: [],
|
|
678
|
+
order,
|
|
679
|
+
hasMore: () => false,
|
|
680
|
+
trimRows: identityRows,
|
|
681
|
+
nextPageToken: () => undefined,
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const compiledInputs = inputs.map((input) =>
|
|
686
|
+
compileObjectQueryInternal(projectId, input, exactContext)
|
|
687
|
+
)
|
|
688
|
+
const identities = compileSetIdentities(op, compiledInputs)
|
|
689
|
+
const order = compileOrder(identityOrderFields())
|
|
690
|
+
const selectedOrder = compileOrder(identityOrderFields(), "selected")
|
|
691
|
+
const sql = `
|
|
692
|
+
SELECT selected.*, selected.properties AS _cursor_properties
|
|
693
|
+
FROM (${identities.sql}) AS ids
|
|
694
|
+
JOIN objects AS selected
|
|
695
|
+
ON selected.project_id = ?
|
|
696
|
+
AND selected.object_type_id = ids.object_type_id
|
|
697
|
+
AND selected.primary_id = ids.primary_id
|
|
698
|
+
ORDER BY ${selectedOrder.sql}
|
|
699
|
+
`
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
sql,
|
|
703
|
+
args: [...identities.args, projectId, ...selectedOrder.args],
|
|
704
|
+
totalSql: `SELECT COUNT(*)::bigint AS total FROM (${identities.sql}) AS ids`,
|
|
705
|
+
totalArgs: identities.args,
|
|
706
|
+
order,
|
|
707
|
+
hasMore: () => false,
|
|
708
|
+
trimRows: identityRows,
|
|
709
|
+
nextPageToken: () => undefined,
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function compileProject(
|
|
714
|
+
projectId: string,
|
|
715
|
+
inputQuery: ObjectQuery,
|
|
716
|
+
properties: readonly string[] | undefined,
|
|
717
|
+
ctx: CompileContext
|
|
718
|
+
): CompiledPgObjectQuery {
|
|
719
|
+
const input = compileObjectQueryInternal(projectId, inputQuery, ctx)
|
|
720
|
+
if (!properties) return input
|
|
721
|
+
|
|
722
|
+
const projection = compileProjectionExpression(properties)
|
|
723
|
+
const inputOrder = compileOrder(input.order.fields, "input", "input._cursor_properties")
|
|
724
|
+
const outputOrder = compileOrder(input.order.fields, undefined, "_cursor_properties")
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
sql: `
|
|
728
|
+
SELECT
|
|
729
|
+
input.project_id,
|
|
730
|
+
input.object_type_id,
|
|
731
|
+
input.primary_id,
|
|
732
|
+
${projection.sql} AS properties,
|
|
733
|
+
input._cursor_properties AS _cursor_properties,
|
|
734
|
+
input.created_at,
|
|
735
|
+
input.updated_at,
|
|
736
|
+
input.version,
|
|
737
|
+
input.last_commit_id
|
|
738
|
+
FROM (${input.sql}) AS input
|
|
739
|
+
ORDER BY ${inputOrder.sql}
|
|
740
|
+
`,
|
|
741
|
+
args: [...projection.args, ...input.args, ...inputOrder.args],
|
|
742
|
+
totalSql: input.totalSql,
|
|
743
|
+
totalArgs: input.totalArgs,
|
|
744
|
+
order: outputOrder,
|
|
745
|
+
hasMoreProbe: input.hasMoreProbe,
|
|
746
|
+
hasMore: input.hasMore,
|
|
747
|
+
trimRows: input.trimRows,
|
|
748
|
+
nextPageToken: input.nextPageToken,
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function compileAggregateSource(projectId: string, query: ObjectQuery): CompiledAggregateSource {
|
|
753
|
+
switch (query.kind) {
|
|
754
|
+
case "start":
|
|
755
|
+
return compileAggregateStart(projectId, query)
|
|
756
|
+
case "filter":
|
|
757
|
+
return compileAggregateWhere(projectId, query.input, compilePredicate(query.predicate))
|
|
758
|
+
case "text": {
|
|
759
|
+
return compileAggregateWhere(
|
|
760
|
+
projectId,
|
|
761
|
+
query.input,
|
|
762
|
+
compileTextSearchPredicate(query.query, query.fields, query.fieldsByObjectType)
|
|
763
|
+
)
|
|
764
|
+
}
|
|
765
|
+
case "traverse":
|
|
766
|
+
return compileAggregateTraversal(
|
|
767
|
+
projectId,
|
|
768
|
+
query.input,
|
|
769
|
+
query.linkId,
|
|
770
|
+
query.direction,
|
|
771
|
+
query.sourceObjectTypeId
|
|
772
|
+
)
|
|
773
|
+
case "set":
|
|
774
|
+
return compileAggregateSet(projectId, query.op, query.inputs)
|
|
775
|
+
case "sort":
|
|
776
|
+
case "project":
|
|
777
|
+
// `expand` is output-shaping, like sort/project: aggregates ignore it.
|
|
778
|
+
case "expand":
|
|
779
|
+
return compileAggregateSource(projectId, query.input)
|
|
780
|
+
case "limit":
|
|
781
|
+
case "page":
|
|
782
|
+
return compileRowQueryAggregateSource(projectId, query)
|
|
783
|
+
case "vector":
|
|
784
|
+
throw new Error(`[SixbPg] PostgreSQL object storage does not support query node 'vector'`)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function compileAggregateStart(
|
|
789
|
+
projectId: string,
|
|
790
|
+
query: Extract<ObjectQuery, { kind: "start" }>
|
|
791
|
+
): CompiledAggregateSource {
|
|
792
|
+
if (query.includeSubtypes === true) {
|
|
793
|
+
throw new Error("[SixbPg] PostgreSQL object storage does not support start.includeSubtypes")
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
sql: `
|
|
798
|
+
SELECT project_id, object_type_id, primary_id, properties
|
|
799
|
+
FROM objects
|
|
800
|
+
WHERE project_id = ? AND object_type_id = ?
|
|
801
|
+
`,
|
|
802
|
+
args: [projectId, query.objectTypeId],
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function compileAggregateWhere(
|
|
807
|
+
projectId: string,
|
|
808
|
+
inputQuery: ObjectQuery,
|
|
809
|
+
predicate: CompiledPredicate
|
|
810
|
+
): CompiledAggregateSource {
|
|
811
|
+
const input = compileAggregateSource(projectId, inputQuery)
|
|
812
|
+
return {
|
|
813
|
+
sql: `
|
|
814
|
+
SELECT input.project_id, input.object_type_id, input.primary_id, input.properties
|
|
815
|
+
FROM (${input.sql}) AS input
|
|
816
|
+
WHERE ${predicate.sql}
|
|
817
|
+
`,
|
|
818
|
+
args: [...input.args, ...predicate.args],
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function compileAggregateTraversal(
|
|
823
|
+
projectId: string,
|
|
824
|
+
inputQuery: ObjectQuery,
|
|
825
|
+
linkId: string,
|
|
826
|
+
direction: "outgoing" | "incoming",
|
|
827
|
+
sourceObjectTypeId?: string
|
|
828
|
+
): CompiledAggregateSource {
|
|
829
|
+
const input = compileAggregateSource(projectId, inputQuery)
|
|
830
|
+
const outputAlias = direction === "outgoing" ? "target_object" : "source_object"
|
|
831
|
+
const joinSql =
|
|
832
|
+
direction === "outgoing"
|
|
833
|
+
? `
|
|
834
|
+
JOIN links AS edge
|
|
835
|
+
ON edge.project_id = input.project_id
|
|
836
|
+
AND edge.source_type_id = input.object_type_id
|
|
837
|
+
AND edge.source_id = input.primary_id
|
|
838
|
+
AND edge.link_id = ?
|
|
839
|
+
JOIN objects AS target_object
|
|
840
|
+
ON target_object.project_id = edge.project_id
|
|
841
|
+
AND target_object.object_type_id = edge.target_type_id
|
|
842
|
+
AND target_object.primary_id = edge.target_id
|
|
843
|
+
`
|
|
844
|
+
: `
|
|
845
|
+
JOIN links AS edge
|
|
846
|
+
ON edge.project_id = input.project_id
|
|
847
|
+
AND edge.target_type_id = input.object_type_id
|
|
848
|
+
AND edge.target_id = input.primary_id
|
|
849
|
+
AND edge.link_id = ?${sourceObjectTypeId === undefined ? "" : "\n AND edge.source_type_id = ?"}
|
|
850
|
+
JOIN objects AS source_object
|
|
851
|
+
ON source_object.project_id = edge.project_id
|
|
852
|
+
AND source_object.object_type_id = edge.source_type_id
|
|
853
|
+
AND source_object.primary_id = edge.source_id
|
|
854
|
+
`
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
sql: `
|
|
858
|
+
SELECT DISTINCT
|
|
859
|
+
${outputAlias}.project_id,
|
|
860
|
+
${outputAlias}.object_type_id,
|
|
861
|
+
${outputAlias}.primary_id,
|
|
862
|
+
${outputAlias}.properties
|
|
863
|
+
FROM (${input.sql}) AS input
|
|
864
|
+
${joinSql}
|
|
865
|
+
`,
|
|
866
|
+
args: [
|
|
867
|
+
...input.args,
|
|
868
|
+
linkId,
|
|
869
|
+
...(sourceObjectTypeId === undefined ? [] : [sourceObjectTypeId]),
|
|
870
|
+
],
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function compileAggregateSet(
|
|
875
|
+
projectId: string,
|
|
876
|
+
op: ObjectQuerySetOperation,
|
|
877
|
+
inputs: readonly ObjectQuery[]
|
|
878
|
+
): CompiledAggregateSource {
|
|
879
|
+
if (inputs.length === 0) {
|
|
880
|
+
return {
|
|
881
|
+
sql: `
|
|
882
|
+
SELECT project_id, object_type_id, primary_id, properties
|
|
883
|
+
FROM objects
|
|
884
|
+
WHERE 1 = 0
|
|
885
|
+
`,
|
|
886
|
+
args: [],
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const compiledInputs = inputs.map((input) => compileAggregateSource(projectId, input))
|
|
891
|
+
const identities = compileSetIdentities(op, compiledInputs)
|
|
892
|
+
return {
|
|
893
|
+
sql: `
|
|
894
|
+
SELECT selected.project_id, selected.object_type_id, selected.primary_id, selected.properties
|
|
895
|
+
FROM (${identities.sql}) AS ids
|
|
896
|
+
JOIN objects AS selected
|
|
897
|
+
ON selected.project_id = ?
|
|
898
|
+
AND selected.object_type_id = ids.object_type_id
|
|
899
|
+
AND selected.primary_id = ids.primary_id
|
|
900
|
+
`,
|
|
901
|
+
args: [...identities.args, projectId],
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function compileRowQueryAggregateSource(
|
|
906
|
+
projectId: string,
|
|
907
|
+
query: ObjectQuery
|
|
908
|
+
): CompiledAggregateSource {
|
|
909
|
+
const rowQuery = compileObjectQueryInternal(projectId, query, exactContext)
|
|
910
|
+
return {
|
|
911
|
+
sql: `
|
|
912
|
+
SELECT input.project_id, input.object_type_id, input.primary_id, input.properties
|
|
913
|
+
FROM (${rowQuery.sql}) AS input
|
|
914
|
+
`,
|
|
915
|
+
args: rowQuery.args,
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function needsLimitProbe(query: ObjectQuery): boolean {
|
|
920
|
+
switch (query.kind) {
|
|
921
|
+
case "limit":
|
|
922
|
+
return true
|
|
923
|
+
case "project":
|
|
924
|
+
case "sort":
|
|
925
|
+
return needsLimitProbe(query.input)
|
|
926
|
+
default:
|
|
927
|
+
return false
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function hasMoreProbeFor(compiled: CompiledPgObjectQuery): CompiledHasMoreProbe {
|
|
932
|
+
return (
|
|
933
|
+
compiled.hasMoreProbe ?? {
|
|
934
|
+
sql: compiled.sql,
|
|
935
|
+
args: compiled.args,
|
|
936
|
+
hasMore: (rowCount) => compiled.hasMore(rowCount, undefined),
|
|
937
|
+
}
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function compileSetIdentities(
|
|
942
|
+
op: ObjectQuerySetOperation,
|
|
943
|
+
inputs: readonly { sql: string; args: readonly unknown[] }[]
|
|
944
|
+
): CompiledPredicate {
|
|
945
|
+
const operator = op === "union" ? "UNION" : op === "intersect" ? "INTERSECT" : "EXCEPT"
|
|
946
|
+
const identitySqls = inputs.map(
|
|
947
|
+
(input) => `SELECT object_type_id, primary_id FROM (${input.sql}) AS input`
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
sql: identitySqls.join(` ${operator} `),
|
|
952
|
+
args: inputs.flatMap((input) => input.args),
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function compileProjectionExpression(properties: readonly string[]): CompiledPredicate {
|
|
957
|
+
if (properties.length === 0) return { sql: "'{}'::jsonb", args: [] }
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
sql: `
|
|
961
|
+
COALESCE(
|
|
962
|
+
(
|
|
963
|
+
SELECT jsonb_object_agg(projected.key, projected.value)
|
|
964
|
+
FROM jsonb_each(input.properties) AS projected(key, value)
|
|
965
|
+
WHERE projected.key IN (${properties.map(() => "?::text").join(", ")})
|
|
966
|
+
),
|
|
967
|
+
'{}'::jsonb
|
|
968
|
+
)
|
|
969
|
+
`,
|
|
970
|
+
args: [...properties],
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
|
|
975
|
+
switch (predicate.op) {
|
|
976
|
+
case "and":
|
|
977
|
+
case "or": {
|
|
978
|
+
const items = predicate.items.map(compilePredicate)
|
|
979
|
+
const joiner = predicate.op === "and" ? " AND " : " OR "
|
|
980
|
+
return {
|
|
981
|
+
sql: `(${items.map((item) => item.sql).join(joiner)})`,
|
|
982
|
+
args: items.flatMap((item) => item.args),
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
case "not": {
|
|
986
|
+
const item = compilePredicate(predicate.item)
|
|
987
|
+
return negatePredicate(item)
|
|
988
|
+
}
|
|
989
|
+
case "eq":
|
|
990
|
+
return predicate.scalarKind === "decimal"
|
|
991
|
+
? compileDecimalEqualityPredicate(predicate.propertyId, "eq", predicate.value)
|
|
992
|
+
: compileEqualityPredicate(predicate.propertyId, "eq", predicate.value)
|
|
993
|
+
case "neq":
|
|
994
|
+
return predicate.scalarKind === "decimal"
|
|
995
|
+
? compileDecimalEqualityPredicate(predicate.propertyId, "neq", predicate.value)
|
|
996
|
+
: compileEqualityPredicate(predicate.propertyId, "neq", predicate.value)
|
|
997
|
+
case "lt":
|
|
998
|
+
case "lte":
|
|
999
|
+
case "gt":
|
|
1000
|
+
case "gte": {
|
|
1001
|
+
const op = sqlComparisonOperator(predicate.op)
|
|
1002
|
+
if (predicate.scalarKind === "decimal") {
|
|
1003
|
+
return {
|
|
1004
|
+
sql: `(${jsonTypeExpression()} = 'string' AND (${jsonTextExpression()})::numeric ${op} ?::numeric)`,
|
|
1005
|
+
args: [predicate.propertyId, predicate.propertyId, predicate.value],
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
sql: `${jsonValueExpression()} ${op} ?::text::jsonb`,
|
|
1010
|
+
args: [predicate.propertyId, jsonbValue(predicate.value)],
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
case "in":
|
|
1014
|
+
return predicate.scalarKind === "decimal"
|
|
1015
|
+
? compileDecimalInPredicate(predicate.propertyId, predicate.values)
|
|
1016
|
+
: compileInPredicate(predicate.propertyId, predicate.values)
|
|
1017
|
+
case "exists": {
|
|
1018
|
+
const sql = `jsonb_exists(properties, ?::text)`
|
|
1019
|
+
const item: CompiledPredicate = { sql, args: [predicate.propertyId] }
|
|
1020
|
+
return predicate.value ? item : negatePredicate(item)
|
|
1021
|
+
}
|
|
1022
|
+
case "contains":
|
|
1023
|
+
return compileContainsPredicate(predicate.propertyId, predicate.value)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function compileDecimalEqualityPredicate(
|
|
1028
|
+
propertyId: string,
|
|
1029
|
+
op: "eq" | "neq",
|
|
1030
|
+
value: unknown
|
|
1031
|
+
): CompiledPredicate {
|
|
1032
|
+
if (value === null) {
|
|
1033
|
+
const sql = `${jsonTypeExpression()} = 'null'`
|
|
1034
|
+
const item: CompiledPredicate = { sql, args: [propertyId] }
|
|
1035
|
+
return op === "eq" ? item : negatePredicate(item)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (op === "eq") {
|
|
1039
|
+
return {
|
|
1040
|
+
sql: `(${jsonTypeExpression()} = 'string' AND (${jsonTextExpression()})::numeric = ?::numeric)`,
|
|
1041
|
+
args: [propertyId, propertyId, value],
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
sql: `(${jsonTypeExpression()} IS NULL OR ${jsonTypeExpression()} = 'null' OR ${jsonTypeExpression()} <> 'string' OR (${jsonTextExpression()})::numeric <> ?::numeric)`,
|
|
1047
|
+
args: [propertyId, propertyId, propertyId, propertyId, value],
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function compileDecimalInPredicate(
|
|
1052
|
+
propertyId: string,
|
|
1053
|
+
values: readonly unknown[]
|
|
1054
|
+
): CompiledPredicate {
|
|
1055
|
+
if (values.length === 0) return { sql: "0 = 1", args: [] }
|
|
1056
|
+
const nonNullValues = values.filter((value) => value !== null)
|
|
1057
|
+
const clauses: string[] = []
|
|
1058
|
+
const args: unknown[] = []
|
|
1059
|
+
|
|
1060
|
+
if (nonNullValues.length !== values.length) {
|
|
1061
|
+
clauses.push(`${jsonTypeExpression()} = 'null'`)
|
|
1062
|
+
args.push(propertyId)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (nonNullValues.length > 0) {
|
|
1066
|
+
clauses.push(
|
|
1067
|
+
`(${jsonTypeExpression()} = 'string' AND (${jsonTextExpression()})::numeric IN (${nonNullValues
|
|
1068
|
+
.map(() => "?::numeric")
|
|
1069
|
+
.join(", ")}))`
|
|
1070
|
+
)
|
|
1071
|
+
args.push(propertyId, propertyId, ...nonNullValues)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
sql: `(${clauses.join(" OR ")})`,
|
|
1076
|
+
args,
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function negatePredicate(predicate: CompiledPredicate): CompiledPredicate {
|
|
1081
|
+
return {
|
|
1082
|
+
sql: `(NOT COALESCE((${predicate.sql}), false))`,
|
|
1083
|
+
args: predicate.args,
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function compileEqualityPredicate(
|
|
1088
|
+
propertyId: string,
|
|
1089
|
+
op: "eq" | "neq",
|
|
1090
|
+
value: unknown
|
|
1091
|
+
): CompiledPredicate {
|
|
1092
|
+
if (value === null) {
|
|
1093
|
+
const sql = `${jsonTypeExpression()} = 'null'`
|
|
1094
|
+
const item: CompiledPredicate = { sql, args: [propertyId] }
|
|
1095
|
+
return op === "eq" ? item : negatePredicate(item)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (typeof value === "string") {
|
|
1099
|
+
if (op === "eq") {
|
|
1100
|
+
return {
|
|
1101
|
+
sql: `(${jsonTypeExpression()} = 'string' AND ${jsonTextExpression()} = ?::text)`,
|
|
1102
|
+
args: [propertyId, propertyId, value],
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return {
|
|
1107
|
+
sql: `(${jsonTypeExpression()} IS NULL OR ${jsonTypeExpression()} = 'null' OR ${jsonTypeExpression()} <> 'string' OR ${jsonTextExpression()} <> ?::text)`,
|
|
1108
|
+
args: [propertyId, propertyId, propertyId, propertyId, value],
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (op === "eq") {
|
|
1113
|
+
return {
|
|
1114
|
+
sql: `${jsonValueExpression()} = ?::text::jsonb`,
|
|
1115
|
+
args: [propertyId, jsonbValue(value)],
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return {
|
|
1120
|
+
sql: `(${jsonTypeExpression()} IS NULL OR ${jsonTypeExpression()} = 'null' OR ${jsonValueExpression()} <> ?::text::jsonb)`,
|
|
1121
|
+
args: [propertyId, propertyId, propertyId, jsonbValue(value)],
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function compileInPredicate(propertyId: string, values: readonly unknown[]): CompiledPredicate {
|
|
1126
|
+
if (values.length === 0) return { sql: "0 = 1", args: [] }
|
|
1127
|
+
|
|
1128
|
+
const nonNullValues = values.filter((value) => value !== null)
|
|
1129
|
+
const stringValues = nonNullValues.filter((value): value is string => typeof value === "string")
|
|
1130
|
+
const jsonbValues = nonNullValues.filter((value) => typeof value !== "string")
|
|
1131
|
+
const clauses: string[] = []
|
|
1132
|
+
const args: unknown[] = []
|
|
1133
|
+
|
|
1134
|
+
if (values.length !== nonNullValues.length) {
|
|
1135
|
+
clauses.push(`${jsonTypeExpression()} = 'null'`)
|
|
1136
|
+
args.push(propertyId)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (stringValues.length > 0) {
|
|
1140
|
+
clauses.push(
|
|
1141
|
+
`(${jsonTypeExpression()} = 'string' AND ${jsonTextExpression()} IN (${stringValues
|
|
1142
|
+
.map(() => "?::text")
|
|
1143
|
+
.join(", ")}))`
|
|
1144
|
+
)
|
|
1145
|
+
args.push(propertyId, propertyId, ...stringValues)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (jsonbValues.length > 0) {
|
|
1149
|
+
clauses.push(
|
|
1150
|
+
`${jsonValueExpression()} IN (${jsonbValues.map(() => "?::text::jsonb").join(", ")})`
|
|
1151
|
+
)
|
|
1152
|
+
args.push(propertyId, ...jsonbValues.map(jsonbValue))
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return {
|
|
1156
|
+
sql: `(${clauses.join(" OR ")})`,
|
|
1157
|
+
args,
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function compileContainsPredicate(propertyId: string, value: unknown): CompiledPredicate {
|
|
1162
|
+
const clauses: string[] = []
|
|
1163
|
+
const args: unknown[] = []
|
|
1164
|
+
|
|
1165
|
+
if (typeof value === "string") {
|
|
1166
|
+
clauses.push(
|
|
1167
|
+
`(${jsonTypeExpression()} = 'string' AND position(?::text in ${jsonTextExpression()}) > 0)`
|
|
1168
|
+
)
|
|
1169
|
+
args.push(propertyId, value, propertyId)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
clauses.push(
|
|
1173
|
+
`(${jsonTypeExpression()} = 'array' AND ${jsonValueExpression()} @> jsonb_build_array(?::text::jsonb))`
|
|
1174
|
+
)
|
|
1175
|
+
args.push(propertyId, propertyId, jsonbValue(value))
|
|
1176
|
+
|
|
1177
|
+
if (typeof value === "string") {
|
|
1178
|
+
clauses.push(
|
|
1179
|
+
`(${jsonTypeExpression()} = 'object' AND jsonb_exists(${jsonValueExpression()}, ?::text))`
|
|
1180
|
+
)
|
|
1181
|
+
args.push(propertyId, propertyId, value)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return { sql: `(${clauses.join(" OR ")})`, args }
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function compileTextPredicate(query: string, fields: readonly string[]): CompiledPredicate {
|
|
1188
|
+
const terms = tokenize(query)
|
|
1189
|
+
if (terms.length === 0) return { sql: "0 = 1", args: [] }
|
|
1190
|
+
|
|
1191
|
+
const clauses = terms.map(() => {
|
|
1192
|
+
const fieldClauses = fields.map(
|
|
1193
|
+
() => `position(?::text in lower(coalesce(${jsonTextExpression()}, ''))) > 0`
|
|
1194
|
+
)
|
|
1195
|
+
return `(${fieldClauses.join(" OR ")})`
|
|
1196
|
+
})
|
|
1197
|
+
const args = terms.flatMap((term) => fields.flatMap((field) => [term, field]))
|
|
1198
|
+
|
|
1199
|
+
return { sql: `(${clauses.join(" AND ")})`, args }
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function compileTextSearchPredicate(
|
|
1203
|
+
query: string,
|
|
1204
|
+
fields: readonly string[] | undefined,
|
|
1205
|
+
fieldsByObjectType: Readonly<Record<string, readonly string[]>> | undefined
|
|
1206
|
+
): CompiledPredicate {
|
|
1207
|
+
const predicate =
|
|
1208
|
+
fields && fields.length > 0
|
|
1209
|
+
? compileTextPredicate(query, fields)
|
|
1210
|
+
: compileScopedTextPredicate(query, fieldsByObjectType)
|
|
1211
|
+
|
|
1212
|
+
if (!predicate) {
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
"[SixbPg] PostgreSQL object text search requires fields or resolved text defaults"
|
|
1215
|
+
)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return predicate
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function compileScopedTextPredicate(
|
|
1222
|
+
query: string,
|
|
1223
|
+
fieldsByObjectType: Readonly<Record<string, readonly string[]>> | undefined
|
|
1224
|
+
): CompiledPredicate | null {
|
|
1225
|
+
const scopedPredicates: CompiledPredicate[] = []
|
|
1226
|
+
|
|
1227
|
+
for (const [objectTypeId, fields] of Object.entries(fieldsByObjectType ?? {})) {
|
|
1228
|
+
if (fields.length === 0) continue
|
|
1229
|
+
const predicate = compileTextPredicate(query, fields)
|
|
1230
|
+
scopedPredicates.push({
|
|
1231
|
+
sql: `(object_type_id = ? AND ${predicate.sql})`,
|
|
1232
|
+
args: [objectTypeId, ...predicate.args],
|
|
1233
|
+
})
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (scopedPredicates.length === 0) return null
|
|
1237
|
+
|
|
1238
|
+
return {
|
|
1239
|
+
sql: `(${scopedPredicates.map((predicate) => predicate.sql).join(" OR ")})`,
|
|
1240
|
+
args: scopedPredicates.flatMap((predicate) => predicate.args),
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function compileOrder(
|
|
1245
|
+
fields: readonly CompiledOrderField[],
|
|
1246
|
+
qualifier?: string,
|
|
1247
|
+
propertyColumn = "properties"
|
|
1248
|
+
): CompiledOrder {
|
|
1249
|
+
const clauses: string[] = []
|
|
1250
|
+
const args: unknown[] = []
|
|
1251
|
+
|
|
1252
|
+
for (const field of fields) {
|
|
1253
|
+
if (field.kind === "column") {
|
|
1254
|
+
clauses.push(`${columnExpression(field.column, qualifier)} ${field.direction.toUpperCase()}`)
|
|
1255
|
+
continue
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const direction = field.direction === "desc" ? "DESC" : "ASC"
|
|
1259
|
+
clauses.push(
|
|
1260
|
+
`CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END ASC`,
|
|
1261
|
+
`${compiledPropertyValueExpression(field, propertyColumn)} ${direction}`
|
|
1262
|
+
)
|
|
1263
|
+
args.push(field.propertyId, field.propertyId, field.propertyId)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return { sql: clauses.join(", "), args, fields, propertyColumn }
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function sortOrderFields(fields: readonly ObjectQuerySortField[]): readonly CompiledOrderField[] {
|
|
1270
|
+
const orderFields: CompiledOrderField[] = []
|
|
1271
|
+
for (const field of fields) {
|
|
1272
|
+
if (field.kind !== "property") {
|
|
1273
|
+
throw new Error("[SixbPg] PostgreSQL object storage does not support relevance sorting")
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
orderFields.push({
|
|
1277
|
+
kind: "property",
|
|
1278
|
+
propertyId: field.propertyId,
|
|
1279
|
+
direction: field.direction === "desc" ? "desc" : "asc",
|
|
1280
|
+
scalarKind: field.scalarKind,
|
|
1281
|
+
})
|
|
1282
|
+
}
|
|
1283
|
+
orderFields.push(...identityOrderFields())
|
|
1284
|
+
return orderFields
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function identityOrderFields(): readonly CompiledOrderField[] {
|
|
1288
|
+
return [
|
|
1289
|
+
{ kind: "column", column: "object_type_id", direction: "asc" },
|
|
1290
|
+
{ kind: "column", column: "primary_id", direction: "asc" },
|
|
1291
|
+
]
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function compileKeysetPredicate(
|
|
1295
|
+
fields: readonly CompiledOrderField[],
|
|
1296
|
+
cursor: readonly EncodedCursorValue[],
|
|
1297
|
+
propertyColumn = "properties"
|
|
1298
|
+
): CompiledPredicate {
|
|
1299
|
+
const disjuncts: CompiledPredicate[] = []
|
|
1300
|
+
|
|
1301
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
1302
|
+
const equalityPredicates = fields
|
|
1303
|
+
.slice(0, index)
|
|
1304
|
+
.map((field, equalityIndex) =>
|
|
1305
|
+
compileFieldEquality(field, cursor[equalityIndex], propertyColumn)
|
|
1306
|
+
)
|
|
1307
|
+
const advancePredicate = compileFieldAfter(fields[index], cursor[index], propertyColumn)
|
|
1308
|
+
if (!advancePredicate) continue
|
|
1309
|
+
|
|
1310
|
+
const parts = [...equalityPredicates, advancePredicate]
|
|
1311
|
+
disjuncts.push({
|
|
1312
|
+
sql: `(${parts.map((part) => part.sql).join(" AND ")})`,
|
|
1313
|
+
args: parts.flatMap((part) => part.args),
|
|
1314
|
+
})
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (disjuncts.length === 0) return { sql: "0 = 1", args: [] }
|
|
1318
|
+
|
|
1319
|
+
return {
|
|
1320
|
+
sql: `(${disjuncts.map((part) => part.sql).join(" OR ")})`,
|
|
1321
|
+
args: disjuncts.flatMap((part) => part.args),
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function compileFieldEquality(
|
|
1326
|
+
field: CompiledOrderField,
|
|
1327
|
+
cursor: EncodedCursorValue,
|
|
1328
|
+
propertyColumn = "properties"
|
|
1329
|
+
): CompiledPredicate {
|
|
1330
|
+
if (field.kind === "column") {
|
|
1331
|
+
return {
|
|
1332
|
+
sql: `${columnExpression(field.column)} = ?`,
|
|
1333
|
+
args: [cursor.value],
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (cursor.nullish) {
|
|
1338
|
+
return {
|
|
1339
|
+
sql: `(${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null')`,
|
|
1340
|
+
args: [field.propertyId, field.propertyId],
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return {
|
|
1345
|
+
sql: `(${jsonTypeExpression(propertyColumn)} IS NOT NULL AND ${jsonTypeExpression(propertyColumn)} != 'null' AND ${compiledPropertyValueExpression(field, propertyColumn)} = ${field.scalarKind === "decimal" ? "?::numeric" : "?::text::jsonb"})`,
|
|
1346
|
+
args: [
|
|
1347
|
+
field.propertyId,
|
|
1348
|
+
field.propertyId,
|
|
1349
|
+
field.propertyId,
|
|
1350
|
+
field.scalarKind === "decimal" ? cursor.value : jsonbValue(cursor.value),
|
|
1351
|
+
],
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function compileFieldAfter(
|
|
1356
|
+
field: CompiledOrderField,
|
|
1357
|
+
cursor: EncodedCursorValue,
|
|
1358
|
+
propertyColumn = "properties"
|
|
1359
|
+
): CompiledPredicate | null {
|
|
1360
|
+
if (field.kind === "column") {
|
|
1361
|
+
return {
|
|
1362
|
+
sql: `${columnExpression(field.column)} ${field.direction === "desc" ? "<" : ">"} ?`,
|
|
1363
|
+
args: [cursor.value],
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (cursor.nullish) return null
|
|
1368
|
+
|
|
1369
|
+
const rankSql = `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
|
|
1370
|
+
const valueOperator = field.direction === "desc" ? "<" : ">"
|
|
1371
|
+
|
|
1372
|
+
return {
|
|
1373
|
+
sql: `(${rankSql} > 0 OR (${rankSql} = 0 AND ${compiledPropertyValueExpression(field, propertyColumn)} ${valueOperator} ${field.scalarKind === "decimal" ? "?::numeric" : "?::text::jsonb"}))`,
|
|
1374
|
+
args: [
|
|
1375
|
+
field.propertyId,
|
|
1376
|
+
field.propertyId,
|
|
1377
|
+
field.propertyId,
|
|
1378
|
+
field.propertyId,
|
|
1379
|
+
field.propertyId,
|
|
1380
|
+
field.scalarKind === "decimal" ? cursor.value : jsonbValue(cursor.value),
|
|
1381
|
+
],
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function encodePageToken(row: PgObjectQueryPageRow, fields: readonly CompiledOrderField[]): string {
|
|
1386
|
+
const properties = objectProperties(row._cursor_properties ?? row.properties)
|
|
1387
|
+
const token: EncodedPageToken = {
|
|
1388
|
+
version: 1,
|
|
1389
|
+
order: fields.map(orderFieldKey),
|
|
1390
|
+
values: fields.map((field) => cursorValueForField(row, properties, field)),
|
|
1391
|
+
}
|
|
1392
|
+
return `${PAGE_TOKEN_PREFIX}${Buffer.from(JSON.stringify(token)).toString("base64url")}`
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function decodePageToken(
|
|
1396
|
+
token: string,
|
|
1397
|
+
fields: readonly CompiledOrderField[]
|
|
1398
|
+
): readonly EncodedCursorValue[] {
|
|
1399
|
+
if (!token.startsWith(PAGE_TOKEN_PREFIX)) {
|
|
1400
|
+
throwInvalidPageToken("Invalid PostgreSQL object query page token")
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const raw = token.slice(PAGE_TOKEN_PREFIX.length)
|
|
1404
|
+
let decoded: unknown
|
|
1405
|
+
try {
|
|
1406
|
+
decoded = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"))
|
|
1407
|
+
} catch {
|
|
1408
|
+
throwInvalidPageToken("Invalid PostgreSQL object query page token")
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (!isEncodedPageToken(decoded)) {
|
|
1412
|
+
throwInvalidPageToken("Invalid PostgreSQL object query page token")
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const expectedOrder = fields.map(orderFieldKey)
|
|
1416
|
+
if (
|
|
1417
|
+
decoded.order.length !== expectedOrder.length ||
|
|
1418
|
+
decoded.order.some((field, index) => field !== expectedOrder[index])
|
|
1419
|
+
) {
|
|
1420
|
+
throwInvalidPageToken("PostgreSQL object query page token does not match query order")
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return decoded.values
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function throwInvalidPageToken(message: string): never {
|
|
1427
|
+
throw new ObjectQueryExecutionError("invalid_page_token", message, "$.pageToken")
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function isEncodedPageToken(value: unknown): value is EncodedPageToken {
|
|
1431
|
+
if (!isPlainObject(value)) return false
|
|
1432
|
+
if (value.version !== 1) return false
|
|
1433
|
+
if (!Array.isArray(value.order) || value.order.some((item) => typeof item !== "string")) {
|
|
1434
|
+
return false
|
|
1435
|
+
}
|
|
1436
|
+
if (!Array.isArray(value.values)) return false
|
|
1437
|
+
return value.values.every(
|
|
1438
|
+
(item) =>
|
|
1439
|
+
isPlainObject(item) &&
|
|
1440
|
+
typeof item.nullish === "boolean" &&
|
|
1441
|
+
(item.nullish || Object.hasOwn(item, "value"))
|
|
1442
|
+
)
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function cursorValueForField(
|
|
1446
|
+
row: PgObjectQueryPageRow,
|
|
1447
|
+
properties: Record<string, unknown>,
|
|
1448
|
+
field: CompiledOrderField
|
|
1449
|
+
): EncodedCursorValue {
|
|
1450
|
+
const value =
|
|
1451
|
+
field.kind === "column"
|
|
1452
|
+
? field.column === "object_type_id"
|
|
1453
|
+
? row.object_type_id
|
|
1454
|
+
: row.primary_id
|
|
1455
|
+
: properties[field.propertyId]
|
|
1456
|
+
|
|
1457
|
+
return value === null || value === undefined ? { nullish: true } : { nullish: false, value }
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function orderFieldKey(field: CompiledOrderField): string {
|
|
1461
|
+
return field.kind === "column"
|
|
1462
|
+
? `column:${field.column}:${field.direction}`
|
|
1463
|
+
: `property:${field.propertyId}:${field.scalarKind ?? "json"}:${field.direction}`
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function compiledPropertyValueExpression(
|
|
1467
|
+
field: Extract<CompiledOrderField, { kind: "property" }>,
|
|
1468
|
+
propertyColumn = "properties"
|
|
1469
|
+
): string {
|
|
1470
|
+
return field.scalarKind === "decimal"
|
|
1471
|
+
? `(${jsonTextExpression(propertyColumn)})::numeric`
|
|
1472
|
+
: jsonValueExpression(propertyColumn)
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function columnExpression(column: "object_type_id" | "primary_id", qualifier?: string): string {
|
|
1476
|
+
return qualifier ? `${qualifier}.${column}` : column
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function jsonValueExpression(column = "properties"): string {
|
|
1480
|
+
return `${column} -> (?::text)`
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function jsonTextExpression(column = "properties"): string {
|
|
1484
|
+
return `${column} ->> (?::text)`
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function jsonTypeExpression(column = "properties"): string {
|
|
1488
|
+
return `jsonb_typeof(${jsonValueExpression(column)})`
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function jsonbValue(value: unknown): string {
|
|
1492
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString())
|
|
1493
|
+
const json = JSON.stringify(value)
|
|
1494
|
+
return json === undefined ? "null" : json
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function tokenize(query: string): string[] {
|
|
1498
|
+
return query
|
|
1499
|
+
.trim()
|
|
1500
|
+
.toLowerCase()
|
|
1501
|
+
.split(/\s+/)
|
|
1502
|
+
.filter((term) => term.length > 0)
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function sqlComparisonOperator(op: "lt" | "lte" | "gt" | "gte"): "<" | "<=" | ">" | ">=" {
|
|
1506
|
+
switch (op) {
|
|
1507
|
+
case "lt":
|
|
1508
|
+
return "<"
|
|
1509
|
+
case "lte":
|
|
1510
|
+
return "<="
|
|
1511
|
+
case "gt":
|
|
1512
|
+
return ">"
|
|
1513
|
+
case "gte":
|
|
1514
|
+
return ">="
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function numberPlaceholders(sql: string): string {
|
|
1519
|
+
let index = 0
|
|
1520
|
+
return sql.replace(/\?/g, () => `$${++index}`)
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function numberCompiledQuery<T extends { sql: string; args: unknown[] }>(compiled: T): T {
|
|
1524
|
+
return {
|
|
1525
|
+
...compiled,
|
|
1526
|
+
sql: numberPlaceholders(compiled.sql),
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function objectProperties(value: unknown): Record<string, unknown> {
|
|
1531
|
+
if (typeof value === "string") {
|
|
1532
|
+
const parsed = JSON.parse(value) as unknown
|
|
1533
|
+
return isPlainObject(parsed) ? parsed : {}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
return isPlainObject(value) ? value : {}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function identityRows<T>(rows: readonly T[]): readonly T[] {
|
|
1540
|
+
return rows
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
1544
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1545
|
+
}
|