@tanstack/electric-db-collection 0.4.10 → 0.5.2
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 +146 -103
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +94 -25
- package/dist/cjs/sql-compiler.cjs +43 -92
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.d.ts +94 -25
- package/dist/esm/electric.js +147 -104
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/sql-compiler.js +43 -92
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +3 -3
- package/src/electric.ts +295 -166
- package/src/sql-compiler.ts +57 -134
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,6 +193,51 @@ function compileFunction(
|
|
|
194
193
|
}
|
|
195
194
|
}
|
|
196
195
|
|
|
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`,
|
|
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
|
+
|
|
197
241
|
const compiledArgs = args.map((arg: IR.BasicExpression) => {
|
|
198
242
|
const compiled = compileBasicExpression(arg, params, encodeColumnName)
|
|
199
243
|
// AND/OR group their children by precedence; NOT already wraps its operand.
|
|
@@ -222,22 +266,6 @@ function compileFunction(
|
|
|
222
266
|
|
|
223
267
|
// Special case for NOT - unary prefix operator
|
|
224
268
|
if (name === `not`) {
|
|
225
|
-
if (compiledArgs.length !== 1) {
|
|
226
|
-
throw new Error(`NOT expects 1 argument`)
|
|
227
|
-
}
|
|
228
|
-
// Check if the argument is IS NULL to generate IS NOT NULL
|
|
229
|
-
const arg = args[0]
|
|
230
|
-
if (arg && arg.type === `func`) {
|
|
231
|
-
const funcArg = arg
|
|
232
|
-
if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {
|
|
233
|
-
const innerArg = compileBasicExpression(
|
|
234
|
-
funcArg.args[0]!,
|
|
235
|
-
params,
|
|
236
|
-
encodeColumnName,
|
|
237
|
-
)
|
|
238
|
-
return `${innerArg} IS NOT NULL`
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
269
|
return `${opName} (${compiledArgs[0]})`
|
|
242
270
|
}
|
|
243
271
|
|
|
@@ -253,125 +281,20 @@ function compileFunction(
|
|
|
253
281
|
}
|
|
254
282
|
const [lhs, rhs] = compiledArgs
|
|
255
283
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (isBooleanComparisonOp(name)) {
|
|
260
|
-
const lhsArg = args[0]
|
|
261
|
-
const rhsArg = args[1]
|
|
262
|
-
|
|
263
|
-
// Check if RHS is a boolean literal value
|
|
264
|
-
if (
|
|
265
|
-
rhsArg &&
|
|
266
|
-
rhsArg.type === `val` &&
|
|
267
|
-
typeof rhsArg.value === `boolean`
|
|
268
|
-
) {
|
|
269
|
-
const boolValue = rhsArg.value
|
|
270
|
-
// Remove the boolean param we just added since we'll transform the expression
|
|
271
|
-
params.pop()
|
|
272
|
-
|
|
273
|
-
// Transform based on operator and boolean value
|
|
274
|
-
// Boolean ordering: false < true
|
|
275
|
-
if (name === `lt`) {
|
|
276
|
-
if (boolValue === true) {
|
|
277
|
-
// lt(col, true) → col = false (only false is less than true)
|
|
278
|
-
params.push(false)
|
|
279
|
-
return `${lhs} = $${params.length}`
|
|
280
|
-
} else {
|
|
281
|
-
// lt(col, false) → nothing is less than false
|
|
282
|
-
return `false`
|
|
283
|
-
}
|
|
284
|
-
} else if (name === `gt`) {
|
|
285
|
-
if (boolValue === false) {
|
|
286
|
-
// gt(col, false) → col = true (only true is greater than false)
|
|
287
|
-
params.push(true)
|
|
288
|
-
return `${lhs} = $${params.length}`
|
|
289
|
-
} else {
|
|
290
|
-
// gt(col, true) → nothing is greater than true
|
|
291
|
-
return `false`
|
|
292
|
-
}
|
|
293
|
-
} else if (name === `lte`) {
|
|
294
|
-
if (boolValue === true) {
|
|
295
|
-
// lte(col, true) → everything is ≤ true
|
|
296
|
-
return `true`
|
|
297
|
-
} else {
|
|
298
|
-
// lte(col, false) → col = false
|
|
299
|
-
params.push(false)
|
|
300
|
-
return `${lhs} = $${params.length}`
|
|
301
|
-
}
|
|
302
|
-
} else if (name === `gte`) {
|
|
303
|
-
if (boolValue === false) {
|
|
304
|
-
// gte(col, false) → everything is ≥ false
|
|
305
|
-
return `true`
|
|
306
|
-
} else {
|
|
307
|
-
// gte(col, true) → col = true
|
|
308
|
-
params.push(true)
|
|
309
|
-
return `${lhs} = $${params.length}`
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
284
|
+
if (name === `in`) {
|
|
285
|
+
const valueArg = args[0]!
|
|
286
|
+
const arrayArg = args[1]!
|
|
313
287
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
lhsArg.type === `val` &&
|
|
318
|
-
typeof lhsArg.value === `boolean`
|
|
319
|
-
) {
|
|
320
|
-
const boolValue = lhsArg.value
|
|
321
|
-
// Remove params for this expression and rebuild
|
|
322
|
-
params.pop() // remove RHS
|
|
323
|
-
params.pop() // remove LHS (boolean)
|
|
324
|
-
|
|
325
|
-
// Recompile RHS to get fresh param
|
|
326
|
-
const rhsCompiled = compileBasicExpression(
|
|
327
|
-
rhsArg!,
|
|
328
|
-
params,
|
|
329
|
-
encodeColumnName,
|
|
330
|
-
)
|
|
288
|
+
if (valueArg.type === `val` && Array.isArray(valueArg.value)) {
|
|
289
|
+
throw new Error(`Array-valued 'in' left operand; expected a scalar`)
|
|
290
|
+
}
|
|
331
291
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (boolValue === true) {
|
|
336
|
-
return `false`
|
|
337
|
-
} else {
|
|
338
|
-
// lt(false, col) → gt(col, false) → col = true
|
|
339
|
-
params.push(true)
|
|
340
|
-
return `${rhsCompiled} = $${params.length}`
|
|
341
|
-
}
|
|
342
|
-
} else if (name === `gt`) {
|
|
343
|
-
// gt(true, col) → lt(col, true) → col = false
|
|
344
|
-
if (boolValue === true) {
|
|
345
|
-
params.push(false)
|
|
346
|
-
return `${rhsCompiled} = $${params.length}`
|
|
347
|
-
} else {
|
|
348
|
-
// gt(false, col) → lt(col, false) → nothing is less than false
|
|
349
|
-
return `false`
|
|
350
|
-
}
|
|
351
|
-
} else if (name === `lte`) {
|
|
352
|
-
if (boolValue === false) {
|
|
353
|
-
// lte(false, col) → gte(col, false) → everything
|
|
354
|
-
return `true`
|
|
355
|
-
} else {
|
|
356
|
-
// lte(true, col) → gte(col, true) → col = true
|
|
357
|
-
params.push(true)
|
|
358
|
-
return `${rhsCompiled} = $${params.length}`
|
|
359
|
-
}
|
|
360
|
-
} else if (name === `gte`) {
|
|
361
|
-
if (boolValue === true) {
|
|
362
|
-
// gte(true, col) → lte(col, true) → everything
|
|
363
|
-
return `true`
|
|
364
|
-
} else {
|
|
365
|
-
// gte(false, col) → lte(col, false) → col = false
|
|
366
|
-
params.push(false)
|
|
367
|
-
return `${rhsCompiled} = $${params.length}`
|
|
368
|
-
}
|
|
369
|
-
}
|
|
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`
|
|
370
295
|
}
|
|
371
|
-
}
|
|
372
296
|
|
|
373
|
-
|
|
374
|
-
if (name === `in`) {
|
|
297
|
+
// Literal value lists and ref/ref membership retain the original = ANY form.
|
|
375
298
|
return `${lhs} ${opName}(${rhs})`
|
|
376
299
|
}
|
|
377
300
|
return `${lhs} ${opName} ${rhs}`
|
|
@@ -402,7 +325,7 @@ function isBinaryOp(name: string): boolean {
|
|
|
402
325
|
* (null comparisons in SQL always evaluate to UNKNOWN)
|
|
403
326
|
*/
|
|
404
327
|
function isComparisonOp(name: string): boolean {
|
|
405
|
-
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]
|
|
328
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `in`]
|
|
406
329
|
return comparisonOps.includes(name)
|
|
407
330
|
}
|
|
408
331
|
|