@tanstack/electric-db-collection 0.4.8 → 0.5.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/dist/cjs/electric.cjs +751 -495
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +94 -25
- package/dist/cjs/sql-compiler.cjs +47 -95
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.d.ts +94 -25
- package/dist/esm/electric.js +753 -497
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/sql-compiler.js +47 -95
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +4 -3
- package/src/electric.ts +1176 -752
- package/src/sql-compiler.ts +74 -137
package/src/sql-compiler.ts
CHANGED
|
@@ -88,7 +88,7 @@ function quoteIdentifier(
|
|
|
88
88
|
encodeColumnName?: ColumnEncoder,
|
|
89
89
|
): string {
|
|
90
90
|
const columnName = encodeColumnName ? encodeColumnName(name) : name
|
|
91
|
-
return `"${columnName}"`
|
|
91
|
+
return `"${columnName.replace(/"/g, `""`)}"`
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
@@ -108,7 +108,6 @@ function compileBasicExpression(
|
|
|
108
108
|
params.push(exp.value)
|
|
109
109
|
return `$${params.length}`
|
|
110
110
|
case `ref`:
|
|
111
|
-
// TODO: doesn't yet support JSON(B) values which could be accessed with nested props
|
|
112
111
|
if (exp.path.length !== 1) {
|
|
113
112
|
throw new Error(
|
|
114
113
|
`Compiler can't handle nested properties: ${exp.path.join(`.`)}`,
|
|
@@ -194,9 +193,68 @@ function compileFunction(
|
|
|
194
193
|
}
|
|
195
194
|
}
|
|
196
195
|
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
if (name === `not`) {
|
|
197
|
+
if (args.length !== 1) throw new Error(`NOT expects 1 argument`)
|
|
198
|
+
const arg = args[0]
|
|
199
|
+
if (
|
|
200
|
+
arg?.type === `func` &&
|
|
201
|
+
(arg.name === `isNull` || arg.name === `isUndefined`)
|
|
202
|
+
) {
|
|
203
|
+
if (arg.args.length !== 1) {
|
|
204
|
+
throw new Error(`${arg.name} expects 1 argument`)
|
|
205
|
+
}
|
|
206
|
+
const innerArg = arg.args[0]!
|
|
207
|
+
const compiled = compileBasicExpression(
|
|
208
|
+
innerArg,
|
|
209
|
+
params,
|
|
210
|
+
encodeColumnName,
|
|
211
|
+
)
|
|
212
|
+
return `${innerArg.type === `func` ? `(${compiled})` : compiled} IS NOT NULL`
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const booleanLiteralIndex = args.findIndex(
|
|
217
|
+
(arg) => arg.type === `val` && typeof arg.value === `boolean`,
|
|
199
218
|
)
|
|
219
|
+
if (
|
|
220
|
+
args.length === 2 &&
|
|
221
|
+
isBooleanComparisonOp(name) &&
|
|
222
|
+
booleanLiteralIndex !== -1
|
|
223
|
+
) {
|
|
224
|
+
const literalValue = (args[booleanLiteralIndex] as IR.Value<boolean>).value
|
|
225
|
+
const valueArg = args[booleanLiteralIndex === 0 ? 1 : 0]!
|
|
226
|
+
const compiled = compileBasicExpression(valueArg, params, encodeColumnName)
|
|
227
|
+
const value = `(${compiled})`
|
|
228
|
+
const lessThan =
|
|
229
|
+
(name === `lt` || name === `lte`) === (booleanLiteralIndex === 1)
|
|
230
|
+
const inclusive = name === `lte` || name === `gte`
|
|
231
|
+
|
|
232
|
+
if (inclusive && literalValue === lessThan) {
|
|
233
|
+
return `${value} = ${value}`
|
|
234
|
+
}
|
|
235
|
+
if (!inclusive && literalValue !== lessThan) {
|
|
236
|
+
return `${value} <> ${value}`
|
|
237
|
+
}
|
|
238
|
+
return `${value} = ${lessThan ? `FALSE` : `TRUE`}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const compiledArgs = args.map((arg: IR.BasicExpression) => {
|
|
242
|
+
const compiled = compileBasicExpression(arg, params, encodeColumnName)
|
|
243
|
+
// AND/OR group their children by precedence; NOT already wraps its operand.
|
|
244
|
+
// In value positions, preserve any nested operator as a single expression.
|
|
245
|
+
return arg.type === `func` &&
|
|
246
|
+
(arg.name === `and` ||
|
|
247
|
+
arg.name === `or` ||
|
|
248
|
+
(name !== `and` &&
|
|
249
|
+
name !== `or` &&
|
|
250
|
+
name !== `not` &&
|
|
251
|
+
(isBinaryOp(arg.name) ||
|
|
252
|
+
arg.name === `not` ||
|
|
253
|
+
arg.name === `isNull` ||
|
|
254
|
+
arg.name === `isUndefined`)))
|
|
255
|
+
? `(${compiled})`
|
|
256
|
+
: compiled
|
|
257
|
+
})
|
|
200
258
|
|
|
201
259
|
// Special case for IS NULL / IS NOT NULL - these are postfix operators
|
|
202
260
|
if (name === `isNull` || name === `isUndefined`) {
|
|
@@ -208,22 +266,6 @@ function compileFunction(
|
|
|
208
266
|
|
|
209
267
|
// Special case for NOT - unary prefix operator
|
|
210
268
|
if (name === `not`) {
|
|
211
|
-
if (compiledArgs.length !== 1) {
|
|
212
|
-
throw new Error(`NOT expects 1 argument`)
|
|
213
|
-
}
|
|
214
|
-
// Check if the argument is IS NULL to generate IS NOT NULL
|
|
215
|
-
const arg = args[0]
|
|
216
|
-
if (arg && arg.type === `func`) {
|
|
217
|
-
const funcArg = arg
|
|
218
|
-
if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {
|
|
219
|
-
const innerArg = compileBasicExpression(
|
|
220
|
-
funcArg.args[0]!,
|
|
221
|
-
params,
|
|
222
|
-
encodeColumnName,
|
|
223
|
-
)
|
|
224
|
-
return `${innerArg} IS NOT NULL`
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
269
|
return `${opName} (${compiledArgs[0]})`
|
|
228
270
|
}
|
|
229
271
|
|
|
@@ -231,7 +273,7 @@ function compileFunction(
|
|
|
231
273
|
// Special handling for AND/OR which can be variadic
|
|
232
274
|
if ((name === `and` || name === `or`) && compiledArgs.length > 2) {
|
|
233
275
|
// Chain multiple arguments: (a AND b AND c) or (a OR b OR c)
|
|
234
|
-
return compiledArgs.
|
|
276
|
+
return compiledArgs.join(` ${opName} `)
|
|
235
277
|
}
|
|
236
278
|
|
|
237
279
|
if (compiledArgs.length !== 2) {
|
|
@@ -239,125 +281,20 @@ function compileFunction(
|
|
|
239
281
|
}
|
|
240
282
|
const [lhs, rhs] = compiledArgs
|
|
241
283
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (isBooleanComparisonOp(name)) {
|
|
246
|
-
const lhsArg = args[0]
|
|
247
|
-
const rhsArg = args[1]
|
|
248
|
-
|
|
249
|
-
// Check if RHS is a boolean literal value
|
|
250
|
-
if (
|
|
251
|
-
rhsArg &&
|
|
252
|
-
rhsArg.type === `val` &&
|
|
253
|
-
typeof rhsArg.value === `boolean`
|
|
254
|
-
) {
|
|
255
|
-
const boolValue = rhsArg.value
|
|
256
|
-
// Remove the boolean param we just added since we'll transform the expression
|
|
257
|
-
params.pop()
|
|
258
|
-
|
|
259
|
-
// Transform based on operator and boolean value
|
|
260
|
-
// Boolean ordering: false < true
|
|
261
|
-
if (name === `lt`) {
|
|
262
|
-
if (boolValue === true) {
|
|
263
|
-
// lt(col, true) → col = false (only false is less than true)
|
|
264
|
-
params.push(false)
|
|
265
|
-
return `${lhs} = $${params.length}`
|
|
266
|
-
} else {
|
|
267
|
-
// lt(col, false) → nothing is less than false
|
|
268
|
-
return `false`
|
|
269
|
-
}
|
|
270
|
-
} else if (name === `gt`) {
|
|
271
|
-
if (boolValue === false) {
|
|
272
|
-
// gt(col, false) → col = true (only true is greater than false)
|
|
273
|
-
params.push(true)
|
|
274
|
-
return `${lhs} = $${params.length}`
|
|
275
|
-
} else {
|
|
276
|
-
// gt(col, true) → nothing is greater than true
|
|
277
|
-
return `false`
|
|
278
|
-
}
|
|
279
|
-
} else if (name === `lte`) {
|
|
280
|
-
if (boolValue === true) {
|
|
281
|
-
// lte(col, true) → everything is ≤ true
|
|
282
|
-
return `true`
|
|
283
|
-
} else {
|
|
284
|
-
// lte(col, false) → col = false
|
|
285
|
-
params.push(false)
|
|
286
|
-
return `${lhs} = $${params.length}`
|
|
287
|
-
}
|
|
288
|
-
} else if (name === `gte`) {
|
|
289
|
-
if (boolValue === false) {
|
|
290
|
-
// gte(col, false) → everything is ≥ false
|
|
291
|
-
return `true`
|
|
292
|
-
} else {
|
|
293
|
-
// gte(col, true) → col = true
|
|
294
|
-
params.push(true)
|
|
295
|
-
return `${lhs} = $${params.length}`
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
284
|
+
if (name === `in`) {
|
|
285
|
+
const valueArg = args[0]!
|
|
286
|
+
const arrayArg = args[1]!
|
|
299
287
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
lhsArg.type === `val` &&
|
|
304
|
-
typeof lhsArg.value === `boolean`
|
|
305
|
-
) {
|
|
306
|
-
const boolValue = lhsArg.value
|
|
307
|
-
// Remove params for this expression and rebuild
|
|
308
|
-
params.pop() // remove RHS
|
|
309
|
-
params.pop() // remove LHS (boolean)
|
|
310
|
-
|
|
311
|
-
// Recompile RHS to get fresh param
|
|
312
|
-
const rhsCompiled = compileBasicExpression(
|
|
313
|
-
rhsArg!,
|
|
314
|
-
params,
|
|
315
|
-
encodeColumnName,
|
|
316
|
-
)
|
|
288
|
+
if (valueArg.type === `val` && Array.isArray(valueArg.value)) {
|
|
289
|
+
throw new Error(`Array-valued 'in' left operand; expected a scalar`)
|
|
290
|
+
}
|
|
317
291
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (boolValue === true) {
|
|
322
|
-
return `false`
|
|
323
|
-
} else {
|
|
324
|
-
// lt(false, col) → gt(col, false) → col = true
|
|
325
|
-
params.push(true)
|
|
326
|
-
return `${rhsCompiled} = $${params.length}`
|
|
327
|
-
}
|
|
328
|
-
} else if (name === `gt`) {
|
|
329
|
-
// gt(true, col) → lt(col, true) → col = false
|
|
330
|
-
if (boolValue === true) {
|
|
331
|
-
params.push(false)
|
|
332
|
-
return `${rhsCompiled} = $${params.length}`
|
|
333
|
-
} else {
|
|
334
|
-
// gt(false, col) → lt(col, false) → nothing is less than false
|
|
335
|
-
return `false`
|
|
336
|
-
}
|
|
337
|
-
} else if (name === `lte`) {
|
|
338
|
-
if (boolValue === false) {
|
|
339
|
-
// lte(false, col) → gte(col, false) → everything
|
|
340
|
-
return `true`
|
|
341
|
-
} else {
|
|
342
|
-
// lte(true, col) → gte(col, true) → col = true
|
|
343
|
-
params.push(true)
|
|
344
|
-
return `${rhsCompiled} = $${params.length}`
|
|
345
|
-
}
|
|
346
|
-
} else if (name === `gte`) {
|
|
347
|
-
if (boolValue === true) {
|
|
348
|
-
// gte(true, col) → lte(col, true) → everything
|
|
349
|
-
return `true`
|
|
350
|
-
} else {
|
|
351
|
-
// gte(false, col) → lte(col, false) → col = false
|
|
352
|
-
params.push(false)
|
|
353
|
-
return `${rhsCompiled} = $${params.length}`
|
|
354
|
-
}
|
|
355
|
-
}
|
|
292
|
+
if (arrayArg.type === `ref` && valueArg.type === `val`) {
|
|
293
|
+
// Resolve literal parameters from the array element type before containment.
|
|
294
|
+
return `${lhs} = ANY(${rhs}) AND ${rhs} @> ARRAY[${lhs}] AND ${rhs} IS NOT NULL`
|
|
356
295
|
}
|
|
357
|
-
}
|
|
358
296
|
|
|
359
|
-
|
|
360
|
-
if (name === `in`) {
|
|
297
|
+
// Literal value lists and ref/ref membership retain the original = ANY form.
|
|
361
298
|
return `${lhs} ${opName}(${rhs})`
|
|
362
299
|
}
|
|
363
300
|
return `${lhs} ${opName} ${rhs}`
|
|
@@ -388,7 +325,7 @@ function isBinaryOp(name: string): boolean {
|
|
|
388
325
|
* (null comparisons in SQL always evaluate to UNKNOWN)
|
|
389
326
|
*/
|
|
390
327
|
function isComparisonOp(name: string): boolean {
|
|
391
|
-
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]
|
|
328
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `in`]
|
|
392
329
|
return comparisonOps.includes(name)
|
|
393
330
|
}
|
|
394
331
|
|