@tanstack/electric-db-collection 0.2.23 → 0.2.25

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.
@@ -20,7 +20,7 @@ function compileSQL(options) {
20
20
  const paramsRecord = params.reduce(
21
21
  (acc, param, index) => {
22
22
  const serialized = pgSerializer.serialize(param);
23
- if (serialized !== ``) {
23
+ if (param != null) {
24
24
  acc[`${index + 1}`] = serialized;
25
25
  }
26
26
  return acc;
@@ -1 +1 @@
1
- {"version":3,"file":"sql-compiler.cjs","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from './pg-serializer'\nimport type { SubsetParams } from '@electric-sql/client'\nimport type { IR, LoadSubsetOptions } from '@tanstack/db'\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\nexport function compileSQL<T>(options: LoadSubsetOptions): SubsetParams {\n const { where, orderBy, limit } = options\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // doesn't work if it goes through aliases because then we need to know the entire query to be able to follow the reference until the base collection (cf. followRef function)\n compiledSQL.where = compileBasicExpression(where, params)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Only include non-empty values in params\n // Empty strings from null/undefined should be omitted\n if (serialized !== ``) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>,\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(name: string): string {\n return `\"${name}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>,\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n // TODO: doesn't yet support JSON(B) values which could be accessed with nested props\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`,\n )\n }\n return quoteIdentifier(exp.path[0]!)\n case `func`:\n return compileFunction(exp, params)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(orderBy: IR.OrderBy, params: Array<unknown>): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params),\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>,\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = [],\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\n\n const compiledArgs = args.map((arg: IR.BasicExpression) =>\n compileBasicExpression(arg, params),\n )\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`NOT expects 1 argument`)\n }\n // Check if the argument is IS NULL to generate IS NOT NULL\n const arg = args[0]\n if (arg && arg.type === `func`) {\n const funcArg = arg\n if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {\n const innerArg = compileBasicExpression(funcArg.args[0]!, params)\n return `${innerArg} IS NOT NULL`\n }\n }\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.map((arg) => `(${arg})`).join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\n // Special case for = ANY operator which needs parentheses around the array parameter\n if (name === `in`) {\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":["serialize"],"mappings":"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"}
1
+ {"version":3,"file":"sql-compiler.cjs","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from './pg-serializer'\nimport type { SubsetParams } from '@electric-sql/client'\nimport type { IR, LoadSubsetOptions } from '@tanstack/db'\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\nexport function compileSQL<T>(options: LoadSubsetOptions): SubsetParams {\n const { where, orderBy, limit } = options\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // doesn't work if it goes through aliases because then we need to know the entire query to be able to follow the reference until the base collection (cf. followRef function)\n compiledSQL.where = compileBasicExpression(where, params)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Empty strings are valid query values (e.g., WHERE column = '')\n // Only omit null/undefined values from params\n if (param != null) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>,\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(name: string): string {\n return `\"${name}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>,\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n // TODO: doesn't yet support JSON(B) values which could be accessed with nested props\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`,\n )\n }\n return quoteIdentifier(exp.path[0]!)\n case `func`:\n return compileFunction(exp, params)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(orderBy: IR.OrderBy, params: Array<unknown>): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params),\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>,\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = [],\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\n\n const compiledArgs = args.map((arg: IR.BasicExpression) =>\n compileBasicExpression(arg, params),\n )\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`NOT expects 1 argument`)\n }\n // Check if the argument is IS NULL to generate IS NOT NULL\n const arg = args[0]\n if (arg && arg.type === `func`) {\n const funcArg = arg\n if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {\n const innerArg = compileBasicExpression(funcArg.args[0]!, params)\n return `${innerArg} IS NOT NULL`\n }\n }\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.map((arg) => `(${arg})`).join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\n // Special case for = ANY operator which needs parentheses around the array parameter\n if (name === `in`) {\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":["serialize"],"mappings":";;;AAQO,SAAS,WAAc,SAA0C;AACtE,QAAM,EAAE,OAAO,SAAS,MAAA,IAAU;AAElC,QAAM,SAAmB,CAAA;AACzB,QAAM,cAAiC,EAAE,OAAA;AAEzC,MAAI,OAAO;AAGT,gBAAY,QAAQ,uBAAuB,OAAO,MAAM;AAAA,EAC1D;AAEA,MAAI,SAAS;AACX,gBAAY,UAAU,eAAe,SAAS,MAAM;AAAA,EACtD;AAEA,MAAI,OAAO;AACT,gBAAY,QAAQ;AAAA,EACtB;AAKA,MAAI,CAAC,OAAO;AACV,gBAAY,QAAQ;AAAA,EACtB;AAIA,QAAM,eAAe,OAAO;AAAA,IAC1B,CAAC,KAAK,OAAO,UAAU;AACrB,YAAM,aAAaA,aAAAA,UAAU,KAAK;AAGlC,UAAI,SAAS,MAAM;AACjB,YAAI,GAAG,QAAQ,CAAC,EAAE,IAAI;AAAA,MACxB;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ;AAAA,EAAA;AAEZ;AAQA,SAAS,gBAAgB,MAAsB;AAC7C,SAAO,IAAI,IAAI;AACjB;AAQA,SAAS,uBACP,KACA,QACQ;AACR,UAAQ,IAAI,MAAA;AAAA,IACV,KAAK;AACH,aAAO,KAAK,IAAI,KAAK;AACrB,aAAO,IAAI,OAAO,MAAM;AAAA,IAC1B,KAAK;AAEH,UAAI,IAAI,KAAK,WAAW,GAAG;AACzB,cAAM,IAAI;AAAA,UACR,4CAA4C,IAAI,KAAK,KAAK,GAAG,CAAC;AAAA,QAAA;AAAA,MAElE;AACA,aAAO,gBAAgB,IAAI,KAAK,CAAC,CAAE;AAAA,IACrC,KAAK;AACH,aAAO,gBAAgB,KAAK,MAAM;AAAA,IACpC;AACE,YAAM,IAAI,MAAM,yBAAyB;AAAA,EAAA;AAE/C;AAEA,SAAS,eAAe,SAAqB,QAAgC;AAC3E,QAAM,yBAAyB,QAAQ;AAAA,IAAI,CAAC,WAC1C,qBAAqB,QAAQ,MAAM;AAAA,EAAA;AAErC,SAAO,uBAAuB,KAAK,GAAG;AACxC;AAEA,SAAS,qBACP,QACA,QACQ;AAGR,QAAM,EAAE,YAAY,eAAA,IAAmB;AACvC,MAAI,MAAM,uBAAuB,YAAY,MAAM;AAEnD,MAAI,eAAe,cAAc,QAAQ;AACvC,UAAM,GAAG,GAAG;AAAA,EACd;AAEA,MAAI,eAAe,UAAU,SAAS;AACpC,UAAM,GAAG,GAAG;AAAA,EACd;AAEA,MAAI,eAAe,UAAU,QAAQ;AACnC,UAAM,GAAG,GAAG;AAAA,EACd;AAEA,SAAO;AACT;AAKA,SAAS,YAAY,KAA2C;AAC9D,SAAO,IAAI,SAAS,UAAU,IAAI,UAAU,QAAQ,IAAI,UAAU;AACpE;AAEA,SAAS,gBACP,KACA,SAAyB,IACjB;AACR,QAAM,EAAE,MAAM,KAAA,IAAS;AAEvB,QAAM,SAAS,UAAU,IAAI;AAK7B,MAAI,eAAe,IAAI,GAAG;AACxB,UAAM,eAAe,KAAK;AAAA,MAAU,CAAC,QACnC,YAAY,GAAG;AAAA,IAAA;AAGjB,QAAI,iBAAiB,IAAI;AAGvB,YAAM,IAAI;AAAA,QACR,yCAAyC,IAAI;AAAA,MAAA;AAAA,IAKjD;AAAA,EACF;AAEA,QAAM,eAAe,KAAK;AAAA,IAAI,CAAC,QAC7B,uBAAuB,KAAK,MAAM;AAAA,EAAA;AAIpC,MAAI,SAAS,YAAY,SAAS,eAAe;AAC/C,QAAI,aAAa,WAAW,GAAG;AAC7B,YAAM,IAAI,MAAM,GAAG,IAAI,qBAAqB;AAAA,IAC9C;AACA,WAAO,GAAG,aAAa,CAAC,CAAC,IAAI,MAAM;AAAA,EACrC;AAGA,MAAI,SAAS,OAAO;AAClB,QAAI,aAAa,WAAW,GAAG;AAC7B,YAAM,IAAI,MAAM,wBAAwB;AAAA,IAC1C;AAEA,UAAM,MAAM,KAAK,CAAC;AAClB,QAAI,OAAO,IAAI,SAAS,QAAQ;AAC9B,YAAM,UAAU;AAChB,UAAI,QAAQ,SAAS,YAAY,QAAQ,SAAS,eAAe;AAC/D,cAAM,WAAW,uBAAuB,QAAQ,KAAK,CAAC,GAAI,MAAM;AAChE,eAAO,GAAG,QAAQ;AAAA,MACpB;AAAA,IACF;AACA,WAAO,GAAG,MAAM,KAAK,aAAa,CAAC,CAAC;AAAA,EACtC;AAEA,MAAI,WAAW,IAAI,GAAG;AAEpB,SAAK,SAAS,SAAS,SAAS,SAAS,aAAa,SAAS,GAAG;AAEhE,aAAO,aAAa,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,EAAE,KAAK,IAAI,MAAM,GAAG;AAAA,IACjE;AAEA,QAAI,aAAa,WAAW,GAAG;AAC7B,YAAM,IAAI,MAAM,mBAAmB,IAAI,sBAAsB;AAAA,IAC/D;AACA,UAAM,CAAC,KAAK,GAAG,IAAI;AAKnB,QAAI,sBAAsB,IAAI,GAAG;AAC/B,YAAM,SAAS,KAAK,CAAC;AACrB,YAAM,SAAS,KAAK,CAAC;AAGrB,UACE,UACA,OAAO,SAAS,SAChB,OAAO,OAAO,UAAU,WACxB;AACA,cAAM,YAAY,OAAO;AAEzB,eAAO,IAAA;AAIP,YAAI,SAAS,MAAM;AACjB,cAAI,cAAc,MAAM;AAEtB,mBAAO,KAAK,KAAK;AACjB,mBAAO,GAAG,GAAG,OAAO,OAAO,MAAM;AAAA,UACnC,OAAO;AAEL,mBAAO;AAAA,UACT;AAAA,QACF,WAAW,SAAS,MAAM;AACxB,cAAI,cAAc,OAAO;AAEvB,mBAAO,KAAK,IAAI;AAChB,mBAAO,GAAG,GAAG,OAAO,OAAO,MAAM;AAAA,UACnC,OAAO;AAEL,mBAAO;AAAA,UACT;AAAA,QACF,WAAW,SAAS,OAAO;AACzB,cAAI,cAAc,MAAM;AAEtB,mBAAO;AAAA,UACT,OAAO;AAEL,mBAAO,KAAK,KAAK;AACjB,mBAAO,GAAG,GAAG,OAAO,OAAO,MAAM;AAAA,UACnC;AAAA,QACF,WAAW,SAAS,OAAO;AACzB,cAAI,cAAc,OAAO;AAEvB,mBAAO;AAAA,UACT,OAAO;AAEL,mBAAO,KAAK,IAAI;AAChB,mBAAO,GAAG,GAAG,OAAO,OAAO,MAAM;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAGA,UACE,UACA,OAAO,SAAS,SAChB,OAAO,OAAO,UAAU,WACxB;AACA,cAAM,YAAY,OAAO;AAEzB,eAAO,IAAA;AACP,eAAO,IAAA;AAGP,cAAM,cAAc,uBAAuB,QAAS,MAAM;AAG1D,YAAI,SAAS,MAAM;AAEjB,cAAI,cAAc,MAAM;AACtB,mBAAO;AAAA,UACT,OAAO;AAEL,mBAAO,KAAK,IAAI;AAChB,mBAAO,GAAG,WAAW,OAAO,OAAO,MAAM;AAAA,UAC3C;AAAA,QACF,WAAW,SAAS,MAAM;AAExB,cAAI,cAAc,MAAM;AACtB,mBAAO,KAAK,KAAK;AACjB,mBAAO,GAAG,WAAW,OAAO,OAAO,MAAM;AAAA,UAC3C,OAAO;AAEL,mBAAO;AAAA,UACT;AAAA,QACF,WAAW,SAAS,OAAO;AACzB,cAAI,cAAc,OAAO;AAEvB,mBAAO;AAAA,UACT,OAAO;AAEL,mBAAO,KAAK,IAAI;AAChB,mBAAO,GAAG,WAAW,OAAO,OAAO,MAAM;AAAA,UAC3C;AAAA,QACF,WAAW,SAAS,OAAO;AACzB,cAAI,cAAc,MAAM;AAEtB,mBAAO;AAAA,UACT,OAAO;AAEL,mBAAO,KAAK,KAAK;AACjB,mBAAO,GAAG,WAAW,OAAO,OAAO,MAAM;AAAA,UAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,SAAS,MAAM;AACjB,aAAO,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG;AAAA,IAChC;AACA,WAAO,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG;AAAA,EAChC;AAEA,SAAO,GAAG,MAAM,IAAI,aAAa,KAAK,GAAG,CAAC;AAC5C;AAEA,SAAS,WAAW,MAAuB;AACzC,QAAM,YAAY;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEF,SAAO,UAAU,SAAS,IAAI;AAChC;AAOA,SAAS,eAAe,MAAuB;AAC7C,QAAM,gBAAgB,CAAC,MAAM,MAAM,OAAO,MAAM,OAAO,QAAQ,OAAO;AACtE,SAAO,cAAc,SAAS,IAAI;AACpC;AAMA,SAAS,sBAAsB,MAAuB;AACpD,SAAO,CAAC,MAAM,OAAO,MAAM,KAAK,EAAE,SAAS,IAAI;AACjD;AAEA,SAAS,UAAU,MAAsB;AACvC,QAAM,UAAU;AAAA,IACd,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,IAAI;AAAA;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,EAAA;AAGZ,QAAM,SAAS,QAAQ,IAA4B;AAEnD,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,8BAA8B,IAAI,EAAE;AAAA,EACtD;AAEA,SAAO;AACT;;"}
@@ -18,7 +18,7 @@ function compileSQL(options) {
18
18
  const paramsRecord = params.reduce(
19
19
  (acc, param, index) => {
20
20
  const serialized = serialize(param);
21
- if (serialized !== ``) {
21
+ if (param != null) {
22
22
  acc[`${index + 1}`] = serialized;
23
23
  }
24
24
  return acc;
@@ -1 +1 @@
1
- {"version":3,"file":"sql-compiler.js","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from './pg-serializer'\nimport type { SubsetParams } from '@electric-sql/client'\nimport type { IR, LoadSubsetOptions } from '@tanstack/db'\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\nexport function compileSQL<T>(options: LoadSubsetOptions): SubsetParams {\n const { where, orderBy, limit } = options\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // doesn't work if it goes through aliases because then we need to know the entire query to be able to follow the reference until the base collection (cf. followRef function)\n compiledSQL.where = compileBasicExpression(where, params)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Only include non-empty values in params\n // Empty strings from null/undefined should be omitted\n if (serialized !== ``) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>,\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(name: string): string {\n return `\"${name}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>,\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n // TODO: doesn't yet support JSON(B) values which could be accessed with nested props\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`,\n )\n }\n return quoteIdentifier(exp.path[0]!)\n case `func`:\n return compileFunction(exp, params)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(orderBy: IR.OrderBy, params: Array<unknown>): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params),\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>,\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = [],\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\n\n const compiledArgs = args.map((arg: IR.BasicExpression) =>\n compileBasicExpression(arg, params),\n )\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`NOT expects 1 argument`)\n }\n // Check if the argument is IS NULL to generate IS NOT NULL\n const arg = args[0]\n if (arg && arg.type === `func`) {\n const funcArg = arg\n if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {\n const innerArg = compileBasicExpression(funcArg.args[0]!, params)\n return `${innerArg} IS NOT NULL`\n }\n }\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.map((arg) => `(${arg})`).join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\n // Special case for = ANY operator which needs parentheses around the array parameter\n if (name === `in`) {\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sql-compiler.js","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from './pg-serializer'\nimport type { SubsetParams } from '@electric-sql/client'\nimport type { IR, LoadSubsetOptions } from '@tanstack/db'\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\nexport function compileSQL<T>(options: LoadSubsetOptions): SubsetParams {\n const { where, orderBy, limit } = options\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // doesn't work if it goes through aliases because then we need to know the entire query to be able to follow the reference until the base collection (cf. followRef function)\n compiledSQL.where = compileBasicExpression(where, params)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Empty strings are valid query values (e.g., WHERE column = '')\n // Only omit null/undefined values from params\n if (param != null) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>,\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(name: string): string {\n return `\"${name}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>,\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n // TODO: doesn't yet support JSON(B) values which could be accessed with nested props\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`,\n )\n }\n return quoteIdentifier(exp.path[0]!)\n case `func`:\n return compileFunction(exp, params)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(orderBy: IR.OrderBy, params: Array<unknown>): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params),\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>,\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = [],\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\n\n const compiledArgs = args.map((arg: IR.BasicExpression) =>\n compileBasicExpression(arg, params),\n )\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`NOT expects 1 argument`)\n }\n // Check if the argument is IS NULL to generate IS NOT NULL\n const arg = args[0]\n if (arg && arg.type === `func`) {\n const funcArg = arg\n if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {\n const innerArg = compileBasicExpression(funcArg.args[0]!, params)\n return `${innerArg} IS NOT NULL`\n }\n }\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.map((arg) => `(${arg})`).join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\n // Special case for = ANY operator which needs parentheses around the array parameter\n if (name === `in`) {\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/electric-db-collection",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "ElectricSQL collection for TanStack DB",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "@standard-schema/spec": "^1.1.0",
44
44
  "@tanstack/store": "^0.8.0",
45
45
  "debug": "^4.4.3",
46
- "@tanstack/db": "0.5.19"
46
+ "@tanstack/db": "0.5.20"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/debug": "^4.1.12",
@@ -38,9 +38,9 @@ export function compileSQL<T>(options: LoadSubsetOptions): SubsetParams {
38
38
  const paramsRecord = params.reduce(
39
39
  (acc, param, index) => {
40
40
  const serialized = serialize(param)
41
- // Only include non-empty values in params
42
- // Empty strings from null/undefined should be omitted
43
- if (serialized !== ``) {
41
+ // Empty strings are valid query values (e.g., WHERE column = '')
42
+ // Only omit null/undefined values from params
43
+ if (param != null) {
44
44
  acc[`${index + 1}`] = serialized
45
45
  }
46
46
  return acc