@tanstack/powersync-db-collection 0.1.37 → 0.1.38
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/dist/cjs/definitions.cjs.map +1 -1
- package/dist/cjs/definitions.d.cts +34 -3
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/powersync.cjs +233 -78
- package/dist/cjs/powersync.cjs.map +1 -1
- package/dist/cjs/sqlite-compiler.cjs +219 -0
- package/dist/cjs/sqlite-compiler.cjs.map +1 -0
- package/dist/cjs/sqlite-compiler.d.cts +42 -0
- package/dist/esm/definitions.d.ts +34 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/powersync.js +233 -78
- package/dist/esm/powersync.js.map +1 -1
- package/dist/esm/sqlite-compiler.d.ts +42 -0
- package/dist/esm/sqlite-compiler.js +219 -0
- package/dist/esm/sqlite-compiler.js.map +1 -0
- package/package.json +7 -6
- package/src/definitions.ts +40 -2
- package/src/index.ts +1 -0
- package/src/powersync.ts +325 -89
- package/src/sqlite-compiler.ts +354 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { IR, LoadSubsetOptions } from '@tanstack/db'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of compiling LoadSubsetOptions to SQLite
|
|
5
|
+
*/
|
|
6
|
+
export interface SQLiteCompiledQuery {
|
|
7
|
+
/** The WHERE clause (without "WHERE" keyword), e.g., "price > ?" */
|
|
8
|
+
where?: string
|
|
9
|
+
/** The ORDER BY clause (without "ORDER BY" keyword), e.g., "price DESC" */
|
|
10
|
+
orderBy?: string
|
|
11
|
+
/** The LIMIT value */
|
|
12
|
+
limit?: number
|
|
13
|
+
/** Parameter values in order, to be passed to SQLite query */
|
|
14
|
+
params: Array<unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for controlling how SQL is compiled.
|
|
19
|
+
*/
|
|
20
|
+
export interface CompileSQLiteOptions {
|
|
21
|
+
/**
|
|
22
|
+
* When set, column references emit `json_extract(<jsonColumn>, '$.<columnName>')`
|
|
23
|
+
* instead of `"<columnName>"`. The `id` column is excluded since it's stored
|
|
24
|
+
* as a direct column in the tracked table.
|
|
25
|
+
*/
|
|
26
|
+
jsonColumn?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compiles TanStack DB LoadSubsetOptions to SQLite query components.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const compiled = compileSQLite({
|
|
35
|
+
* where: { type: 'func', name: 'gt', args: [
|
|
36
|
+
* { type: 'ref', path: ['price'] },
|
|
37
|
+
* { type: 'val', value: 100 }
|
|
38
|
+
* ]},
|
|
39
|
+
* orderBy: [{ expression: { type: 'ref', path: ['price'] }, compareOptions: { direction: 'desc', nulls: 'last' } }],
|
|
40
|
+
* limit: 50
|
|
41
|
+
* })
|
|
42
|
+
* // Result: { where: '"price" > ?', orderBy: '"price" DESC', limit: 50, params: [100] }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function compileSQLite(
|
|
46
|
+
options: LoadSubsetOptions,
|
|
47
|
+
compileOptions?: CompileSQLiteOptions,
|
|
48
|
+
): SQLiteCompiledQuery {
|
|
49
|
+
const { where, orderBy, limit } = options
|
|
50
|
+
|
|
51
|
+
const params: Array<unknown> = []
|
|
52
|
+
const result: SQLiteCompiledQuery = { params }
|
|
53
|
+
|
|
54
|
+
if (where) {
|
|
55
|
+
result.where = compileExpression(where, params, compileOptions)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (orderBy) {
|
|
59
|
+
result.orderBy = compileOrderBy(orderBy, params, compileOptions)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (limit !== undefined) {
|
|
63
|
+
result.limit = limit
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Quote SQLite identifiers to handle column names correctly.
|
|
71
|
+
* SQLite uses double quotes for identifiers.
|
|
72
|
+
*/
|
|
73
|
+
function quoteIdentifier(name: string): string {
|
|
74
|
+
// Escape any double quotes in the name by doubling them
|
|
75
|
+
const escaped = name.replace(/"/g, `""`)
|
|
76
|
+
return `"${escaped}"`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compiles a BasicExpression to a SQL string, mutating the params array.
|
|
81
|
+
*/
|
|
82
|
+
function compileExpression(
|
|
83
|
+
exp: IR.BasicExpression<unknown>,
|
|
84
|
+
params: Array<unknown>,
|
|
85
|
+
compileOptions?: CompileSQLiteOptions,
|
|
86
|
+
): string {
|
|
87
|
+
switch (exp.type) {
|
|
88
|
+
case `val`:
|
|
89
|
+
params.push(exp.value)
|
|
90
|
+
return `?`
|
|
91
|
+
case `ref`: {
|
|
92
|
+
if (exp.path.length !== 1) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`SQLite compiler doesn't support nested properties: ${exp.path.join(`.`)}`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
const columnName = exp.path[0]!
|
|
98
|
+
if (compileOptions?.jsonColumn && columnName !== `id`) {
|
|
99
|
+
return `json_extract(${compileOptions.jsonColumn}, '$.${columnName}')`
|
|
100
|
+
}
|
|
101
|
+
return quoteIdentifier(columnName)
|
|
102
|
+
}
|
|
103
|
+
case `func`:
|
|
104
|
+
return compileFunction(exp, params, compileOptions)
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown expression type: ${(exp as any).type}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compiles an OrderBy array to a SQL ORDER BY clause.
|
|
112
|
+
*/
|
|
113
|
+
function compileOrderBy(
|
|
114
|
+
orderBy: IR.OrderBy,
|
|
115
|
+
params: Array<unknown>,
|
|
116
|
+
compileOptions?: CompileSQLiteOptions,
|
|
117
|
+
): string {
|
|
118
|
+
const clauses = orderBy.map((clause: IR.OrderByClause) =>
|
|
119
|
+
compileOrderByClause(clause, params, compileOptions),
|
|
120
|
+
)
|
|
121
|
+
return clauses.join(`, `)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compiles a single OrderByClause to SQL.
|
|
126
|
+
*/
|
|
127
|
+
function compileOrderByClause(
|
|
128
|
+
clause: IR.OrderByClause,
|
|
129
|
+
params: Array<unknown>,
|
|
130
|
+
compileOptions?: CompileSQLiteOptions,
|
|
131
|
+
): string {
|
|
132
|
+
const { expression, compareOptions } = clause
|
|
133
|
+
let sql = compileExpression(expression, params, compileOptions)
|
|
134
|
+
|
|
135
|
+
if (compareOptions.direction === `desc`) {
|
|
136
|
+
sql = `${sql} DESC`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// SQLite supports NULLS FIRST/LAST (since 3.30.0)
|
|
140
|
+
if (compareOptions.nulls === `first`) {
|
|
141
|
+
sql = `${sql} NULLS FIRST`
|
|
142
|
+
} else {
|
|
143
|
+
// Default to NULLS LAST (nulls === 'last')
|
|
144
|
+
sql = `${sql} NULLS LAST`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sql
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if a BasicExpression represents a null/undefined value
|
|
152
|
+
*/
|
|
153
|
+
function isNullValue(exp: IR.BasicExpression<unknown>): boolean {
|
|
154
|
+
return exp.type === `val` && (exp.value === null || exp.value === undefined)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Compiles a function expression (operator) to SQL.
|
|
159
|
+
*/
|
|
160
|
+
function compileFunction(
|
|
161
|
+
exp: IR.Func<unknown>,
|
|
162
|
+
params: Array<unknown>,
|
|
163
|
+
compileOptions?: CompileSQLiteOptions,
|
|
164
|
+
): string {
|
|
165
|
+
const { name, args } = exp
|
|
166
|
+
|
|
167
|
+
// Check for null values in comparison operators
|
|
168
|
+
if (isComparisonOp(name)) {
|
|
169
|
+
const hasNullArg = args.some((arg: IR.BasicExpression) => isNullValue(arg))
|
|
170
|
+
if (hasNullArg) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Cannot use null/undefined with '${name}' operator. ` +
|
|
173
|
+
`Use isNull() to check for null values.`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Compile arguments
|
|
179
|
+
const compiledArgs = args.map((arg: IR.BasicExpression) =>
|
|
180
|
+
compileExpression(arg, params, compileOptions),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Handle different operator types
|
|
184
|
+
switch (name) {
|
|
185
|
+
// Binary comparison operators
|
|
186
|
+
case `eq`:
|
|
187
|
+
case `gt`:
|
|
188
|
+
case `gte`:
|
|
189
|
+
case `lt`:
|
|
190
|
+
case `lte`: {
|
|
191
|
+
if (compiledArgs.length !== 2) {
|
|
192
|
+
throw new Error(`${name} expects 2 arguments`)
|
|
193
|
+
}
|
|
194
|
+
const opSymbol = getComparisonOp(name)
|
|
195
|
+
return `${compiledArgs[0]} ${opSymbol} ${compiledArgs[1]}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Logical operators
|
|
199
|
+
case `and`:
|
|
200
|
+
case `or`: {
|
|
201
|
+
if (compiledArgs.length < 2) {
|
|
202
|
+
throw new Error(`${name} expects at least 2 arguments`)
|
|
203
|
+
}
|
|
204
|
+
const opKeyword = name === `and` ? `AND` : `OR`
|
|
205
|
+
return compiledArgs
|
|
206
|
+
.map((arg: string) => `(${arg})`)
|
|
207
|
+
.join(` ${opKeyword} `)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case `not`: {
|
|
211
|
+
if (compiledArgs.length !== 1) {
|
|
212
|
+
throw new Error(`not expects 1 argument`)
|
|
213
|
+
}
|
|
214
|
+
// Check if argument is isNull/isUndefined for IS NOT NULL
|
|
215
|
+
const arg = args[0]
|
|
216
|
+
if (arg && arg.type === `func`) {
|
|
217
|
+
if (arg.name === `isNull` || arg.name === `isUndefined`) {
|
|
218
|
+
const innerArg = compileExpression(
|
|
219
|
+
arg.args[0]!,
|
|
220
|
+
params,
|
|
221
|
+
compileOptions,
|
|
222
|
+
)
|
|
223
|
+
return `${innerArg} IS NOT NULL`
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return `NOT (${compiledArgs[0]})`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Null checking
|
|
230
|
+
case `isNull`:
|
|
231
|
+
case `isUndefined`: {
|
|
232
|
+
if (compiledArgs.length !== 1) {
|
|
233
|
+
throw new Error(`${name} expects 1 argument`)
|
|
234
|
+
}
|
|
235
|
+
return `${compiledArgs[0]} IS NULL`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// IN operator
|
|
239
|
+
case `in`: {
|
|
240
|
+
if (compiledArgs.length !== 2) {
|
|
241
|
+
throw new Error(`in expects 2 arguments (column and array)`)
|
|
242
|
+
}
|
|
243
|
+
// The second argument should be an array value
|
|
244
|
+
// We need to handle this specially - expand the array into multiple placeholders
|
|
245
|
+
const lastParamIndex = params.length - 1
|
|
246
|
+
const arrayValue = params[lastParamIndex]
|
|
247
|
+
|
|
248
|
+
if (!Array.isArray(arrayValue)) {
|
|
249
|
+
throw new Error(`in operator requires an array value`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Remove the array param and add individual values
|
|
253
|
+
params.pop()
|
|
254
|
+
const placeholders = arrayValue.map(() => {
|
|
255
|
+
params.push(arrayValue[params.length - lastParamIndex])
|
|
256
|
+
return `?`
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Re-add individual values properly
|
|
260
|
+
params.length = lastParamIndex // Reset to before array
|
|
261
|
+
for (const val of arrayValue) {
|
|
262
|
+
params.push(val)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return `${compiledArgs[0]} IN (${placeholders.join(`, `)})`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// String operators
|
|
269
|
+
case `like`: {
|
|
270
|
+
if (compiledArgs.length !== 2) {
|
|
271
|
+
throw new Error(`like expects 2 arguments`)
|
|
272
|
+
}
|
|
273
|
+
return `${compiledArgs[0]} LIKE ${compiledArgs[1]}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case `ilike`: {
|
|
277
|
+
if (compiledArgs.length !== 2) {
|
|
278
|
+
throw new Error(`ilike expects 2 arguments`)
|
|
279
|
+
}
|
|
280
|
+
return `${compiledArgs[0]} LIKE ${compiledArgs[1]} COLLATE NOCASE`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// String case functions
|
|
284
|
+
case `upper`: {
|
|
285
|
+
if (compiledArgs.length !== 1) {
|
|
286
|
+
throw new Error(`upper expects 1 argument`)
|
|
287
|
+
}
|
|
288
|
+
return `UPPER(${compiledArgs[0]})`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case `lower`: {
|
|
292
|
+
if (compiledArgs.length !== 1) {
|
|
293
|
+
throw new Error(`lower expects 1 argument`)
|
|
294
|
+
}
|
|
295
|
+
return `LOWER(${compiledArgs[0]})`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
case `length`: {
|
|
299
|
+
if (compiledArgs.length !== 1) {
|
|
300
|
+
throw new Error(`length expects 1 argument`)
|
|
301
|
+
}
|
|
302
|
+
return `LENGTH(${compiledArgs[0]})`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
case `concat`: {
|
|
306
|
+
if (compiledArgs.length < 1) {
|
|
307
|
+
throw new Error(`concat expects at least 1 argument`)
|
|
308
|
+
}
|
|
309
|
+
return `CONCAT(${compiledArgs.join(`, `)})`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case `add`: {
|
|
313
|
+
if (compiledArgs.length !== 2) {
|
|
314
|
+
throw new Error(`add expects 2 arguments`)
|
|
315
|
+
}
|
|
316
|
+
return `${compiledArgs[0]} + ${compiledArgs[1]}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Null fallback
|
|
320
|
+
case `coalesce`: {
|
|
321
|
+
if (compiledArgs.length < 1) {
|
|
322
|
+
throw new Error(`coalesce expects at least 1 argument`)
|
|
323
|
+
}
|
|
324
|
+
return `COALESCE(${compiledArgs.join(`, `)})`
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
default:
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Operator '${name}' is not supported in PowerSync on-demand sync. ` +
|
|
330
|
+
`Supported operators: eq, gt, gte, lt, lte, and, or, not, isNull, in, like, ilike, upper, lower, length, concat, add, coalesce`,
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if operator is a comparison operator
|
|
337
|
+
*/
|
|
338
|
+
function isComparisonOp(name: string): boolean {
|
|
339
|
+
return [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`].includes(name)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get the SQL symbol for a comparison operator
|
|
344
|
+
*/
|
|
345
|
+
function getComparisonOp(name: string): string {
|
|
346
|
+
const ops: Record<string, string> = {
|
|
347
|
+
eq: `=`,
|
|
348
|
+
gt: `>`,
|
|
349
|
+
gte: `>=`,
|
|
350
|
+
lt: `<`,
|
|
351
|
+
lte: `<=`,
|
|
352
|
+
}
|
|
353
|
+
return ops[name]!
|
|
354
|
+
}
|