@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.
@@ -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
- // Special case for comparison operators with boolean values
257
- // PostgreSQL doesn't support < > <= >= on booleans
258
- // Transform to equivalent equality checks or constant expressions
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
- // Check if LHS is a boolean literal value (less common but handle it)
315
- if (
316
- lhsArg &&
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
- // Transform: flip the comparison (val op col → col flipped_op val)
333
- if (name === `lt`) {
334
- // lt(true, col) → gt(col, true) → col > true → nothing is greater than true
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
- // Special case for = ANY operator which needs parentheses around the array parameter
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