From 1b026ac286771d77ca54be6813888de987ac08dc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Dec 2025 14:03:50 +0000 Subject: [PATCH 01/13] feat(db): throw error when JavaScript operators used in queries Adds detection and helpful error messages when users mistakenly use JavaScript operators (||, &&, ??) in query callbacks. These operators are evaluated at query construction time, not execution time, causing silent unexpected behavior. Changes: - Add JavaScriptOperatorInQueryError with helpful suggestions - Add Symbol.toPrimitive trap to RefProxy for primitive coercion - Add checkCallbackForJsOperators() to detect operators in callbacks - Integrate checks into select(), where(), and having() methods - Add comprehensive tests for the new error detection - Fix existing tests that incorrectly used JS operators --- packages/db/src/errors.ts | 24 ++++ packages/db/src/query/builder/index.ts | 11 +- packages/db/src/query/builder/ref-proxy.ts | 110 ++++++++++++++++++ .../db/tests/query/builder/ref-proxy.test.ts | 79 +++++++++++++ .../tests/query/live-query-collection.test.ts | 4 +- packages/db/tests/query/scheduler.test.ts | 4 +- 6 files changed, 227 insertions(+), 5 deletions(-) diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 50cd442ac..592307502 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -398,6 +398,30 @@ export class QueryCompilationError extends TanStackDBError { } } +export class JavaScriptOperatorInQueryError extends QueryBuilderError { + constructor(operator: string, hint?: string) { + const defaultHint = + operator === `||` || operator === `??` + ? `Use coalesce() instead: coalesce(value, defaultValue)` + : operator === `&&` + ? `Use and() for logical conditions` + : operator === `?:` + ? `Use cond() for conditional expressions: cond(condition, trueValue, falseValue)` + : `Use the appropriate query function instead` + + super( + `JavaScript operator "${operator}" cannot be used in queries.\n\n` + + `Query callbacks should only use field references and query functions, not JavaScript logic.\n` + + `${hint || defaultHint}\n\n` + + `Example of incorrect usage:\n` + + ` .select(({users}) => ({ data: users.data || [] }))\n\n` + + `Correct usage:\n` + + ` .select(({users}) => ({ data: coalesce(users.data, []) }))`, + ) + this.name = `JavaScriptOperatorInQueryError` + } +} + export class DistinctRequiresSelectError extends QueryCompilationError { constructor() { super(`DISTINCT requires a SELECT clause.`) diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index e7d2be0c4..eed5af0aa 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -16,7 +16,7 @@ import { QueryMustHaveFromClauseError, SubQueryMustHaveFromClauseError, } from '../../errors.js' -import { createRefProxy, toExpression } from './ref-proxy.js' +import { checkCallbackForJsOperators, createRefProxy, toExpression } from './ref-proxy.js' import type { NamespacedRow, SingleResult } from '../../types.js' import type { Aggregate, @@ -357,6 +357,9 @@ export class BaseQueryBuilder { * ``` */ where(callback: WhereCallback): QueryBuilder { + // Check for JavaScript operators that cannot be translated to query operations + checkCallbackForJsOperators(callback) + const aliases = this._getCurrentAliases() const refProxy = createRefProxy(aliases) as RefsForContext const expression = callback(refProxy) @@ -398,6 +401,9 @@ export class BaseQueryBuilder { * ``` */ having(callback: WhereCallback): QueryBuilder { + // Check for JavaScript operators that cannot be translated to query operations + checkCallbackForJsOperators(callback) + const aliases = this._getCurrentAliases() const refProxy = createRefProxy(aliases) as RefsForContext const expression = callback(refProxy) @@ -447,6 +453,9 @@ export class BaseQueryBuilder { select( callback: (refs: RefsForContext) => TSelectObject, ): QueryBuilder>> { + // Check for JavaScript operators that cannot be translated to query operations + checkCallbackForJsOperators(callback) + const aliases = this._getCurrentAliases() const refProxy = createRefProxy(aliases) as RefsForContext const selectObject = callback(refProxy) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 2f060b5f2..5a0d34161 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -1,7 +1,24 @@ import { PropRef, Value } from '../ir.js' +import { JavaScriptOperatorInQueryError } from '../../errors.js' import type { BasicExpression } from '../ir.js' import type { RefLeaf } from './types.js' +/** + * Creates a handler for Symbol.toPrimitive that throws an error when + * JavaScript tries to coerce a RefProxy to a primitive value. + * This catches misuse like string concatenation, arithmetic, etc. + */ +function createToPrimitiveHandler(path: Array): (hint: string) => never { + return (hint: string) => { + const pathStr = path.length > 0 ? path.join(`.`) : `` + throw new JavaScriptOperatorInQueryError( + hint === `number` ? `arithmetic` : hint === `string` ? `string concatenation` : `comparison`, + `Attempted to use "${pathStr}" in a JavaScript ${hint} context.\n` + + `Query references can only be used with query functions, not JavaScript operators.`, + ) + } +} + export interface RefProxy { /** @internal */ readonly __refProxy: true @@ -44,6 +61,10 @@ export function createSingleRowRefProxy< if (prop === `__refProxy`) return true if (prop === `__path`) return path if (prop === `__type`) return undefined // Type is only for TypeScript inference + // Intercept Symbol.toPrimitive to catch JS coercion attempts + if (prop === Symbol.toPrimitive) { + return createToPrimitiveHandler(path) + } if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const newPath = [...path, String(prop)] @@ -97,6 +118,10 @@ export function createRefProxy>( if (prop === `__refProxy`) return true if (prop === `__path`) return path if (prop === `__type`) return undefined // Type is only for TypeScript inference + // Intercept Symbol.toPrimitive to catch JS coercion attempts + if (prop === Symbol.toPrimitive) { + return createToPrimitiveHandler(path) + } if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const newPath = [...path, String(prop)] @@ -140,6 +165,10 @@ export function createRefProxy>( if (prop === `__refProxy`) return true if (prop === `__path`) return [] if (prop === `__type`) return undefined // Type is only for TypeScript inference + // Intercept Symbol.toPrimitive to catch JS coercion attempts + if (prop === Symbol.toPrimitive) { + return createToPrimitiveHandler([]) + } if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const propStr = String(prop) @@ -213,3 +242,84 @@ export function isRefProxy(value: any): value is RefProxy { export function val(value: T): BasicExpression { return new Value(value) } + +/** + * Patterns that indicate JavaScript operators being used in query callbacks. + * These operators cannot be translated to query operations and will silently + * produce incorrect results. + */ +const JS_OPERATOR_PATTERNS: Array<{ + pattern: RegExp + operator: string + description: string +}> = [ + { + // Match || that's not inside a string or comment + // This regex looks for || not preceded by quotes that would indicate a string + pattern: /\|\|/, + operator: `||`, + description: `logical OR`, + }, + { + // Match && that's not inside a string or comment + pattern: /&&/, + operator: `&&`, + description: `logical AND`, + }, + { + // Match ?? nullish coalescing + pattern: /\?\?/, + operator: `??`, + description: `nullish coalescing`, + }, +] + +/** + * Removes string literals and comments from source code to avoid false positives + * when checking for JavaScript operators. + */ +function stripStringsAndComments(source: string): string { + // Remove template literals (backtick strings) + let result = source.replace(/`(?:[^`\\]|\\.)*`/g, `""`) + // Remove double-quoted strings + result = result.replace(/"(?:[^"\\]|\\.)*"/g, `""`) + // Remove single-quoted strings + result = result.replace(/'(?:[^'\\]|\\.)*'/g, `""`) + // Remove single-line comments + result = result.replace(/\/\/[^\n]*/g, ``) + // Remove multi-line comments + result = result.replace(/\/\*[\s\S]*?\*\//g, ``) + return result +} + +/** + * Checks a callback function's source code for JavaScript operators that + * cannot be translated to query operations. + * + * @param callback - The callback function to check + * @throws JavaScriptOperatorInQueryError if a problematic operator is found + * + * @example + * // This will throw an error: + * checkCallbackForJsOperators(({users}) => users.data || []) + * + * // This is fine: + * checkCallbackForJsOperators(({users}) => users.data) + */ +export function checkCallbackForJsOperators(callback: (...args: Array) => unknown): void { + const source = callback.toString() + + // Strip strings and comments to avoid false positives + const cleanedSource = stripStringsAndComments(source) + + for (const { pattern, operator, description } of JS_OPERATOR_PATTERNS) { + if (pattern.test(cleanedSource)) { + throw new JavaScriptOperatorInQueryError( + operator, + `Found JavaScript ${description} operator (${operator}) in query callback.\n` + + `This operator is evaluated at query construction time, not at query execution time,\n` + + `which means it will not behave as expected.`, + ) + } + } +} diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index ec41351da..0c15016c1 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from 'vitest' import { + checkCallbackForJsOperators, createRefProxy, isRefProxy, toExpression, val, } from '../../../src/query/builder/ref-proxy.js' import { PropRef, Value } from '../../../src/query/ir.js' +import { JavaScriptOperatorInQueryError } from '../../../src/errors.js' describe(`ref-proxy`, () => { describe(`createRefProxy`, () => { @@ -214,4 +216,81 @@ describe(`ref-proxy`, () => { expect((val({ a: 1 }) as Value).value).toEqual({ a: 1 }) }) }) + + describe(`checkCallbackForJsOperators`, () => { + it(`throws error for || operator`, () => { + const callback = ({ users }: any) => ({ data: users.data || [] }) + expect(() => checkCallbackForJsOperators(callback)).toThrow( + JavaScriptOperatorInQueryError, + ) + expect(() => checkCallbackForJsOperators(callback)).toThrow(`||`) + }) + + it(`throws error for && operator`, () => { + const callback = ({ users }: any) => users.active && users.name + expect(() => checkCallbackForJsOperators(callback)).toThrow( + JavaScriptOperatorInQueryError, + ) + expect(() => checkCallbackForJsOperators(callback)).toThrow(`&&`) + }) + + it(`throws error for ?? operator`, () => { + const callback = ({ users }: any) => ({ name: users.name ?? `default` }) + expect(() => checkCallbackForJsOperators(callback)).toThrow( + JavaScriptOperatorInQueryError, + ) + expect(() => checkCallbackForJsOperators(callback)).toThrow(`??`) + }) + + it(`does not throw for valid query callbacks`, () => { + // Simple property access + expect(() => + checkCallbackForJsOperators(({ users }: any) => users.name), + ).not.toThrow() + + // Object with property access + expect(() => + checkCallbackForJsOperators(({ users }: any) => ({ + id: users.id, + name: users.name, + })), + ).not.toThrow() + + // Optional chaining is allowed + expect(() => + checkCallbackForJsOperators(({ users }: any) => users.profile?.bio), + ).not.toThrow() + }) + + it(`does not throw for operators in string literals`, () => { + // || in a string literal should not trigger error + expect(() => + checkCallbackForJsOperators(() => ({ message: `a || b is valid` })), + ).not.toThrow() + + // && in a string literal should not trigger error + expect(() => + checkCallbackForJsOperators(() => ({ message: `a && b is valid` })), + ).not.toThrow() + }) + }) + + describe(`Symbol.toPrimitive trap`, () => { + it(`throws error when proxy is coerced to string`, () => { + const proxy = createRefProxy<{ users: { id: number } }>([`users`]) + expect(() => String(proxy.users.id)).toThrow(JavaScriptOperatorInQueryError) + }) + + it(`throws error when proxy is used in arithmetic`, () => { + const proxy = createRefProxy<{ users: { id: number } }>([`users`]) + expect(() => Number(proxy.users.id)).toThrow(JavaScriptOperatorInQueryError) + }) + + it(`throws error when proxy is concatenated with string`, () => { + const proxy = createRefProxy<{ users: { name: string } }>([`users`]) + expect(() => `Hello ${proxy.users.name}`).toThrow( + JavaScriptOperatorInQueryError, + ) + }) + }) }) diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 8c10c3eb8..df286760b 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -1911,9 +1911,9 @@ describe(`createLiveQueryCollection`, () => { .join({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id)) .select(({ comments: c, users: u }) => ({ id: c.id, - userId: u?.id ?? c.userId, + userId: u.id, text: c.text, - userName: u?.name, + userName: u.name, })), getKey: (item) => item.userId, startSync: true, diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index adb85c979..2de85931e 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection, eq, isNull } from '../../src/query/index.js' +import { coalesce, createLiveQueryCollection, eq, isNull } from '../../src/query/index.js' import { createTransaction } from '../../src/transactions.js' import { createOptimisticAction } from '../../src/optimistic-action.js' import { transactionScopedScheduler } from '../../src/scheduler.js' @@ -721,7 +721,7 @@ describe(`live query scheduler`, () => { .select(({ a, b }) => ({ id: a.id, aValue: a.value, - bValue: b?.value ?? null, + bValue: coalesce(b!.value, null), })), }) From ddc3b47321207aafe64a2e6cb676637aa8a831c3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:08:35 +0000 Subject: [PATCH 02/13] ci: apply automated fixes --- packages/db/src/query/builder/index.ts | 6 +++++- packages/db/src/query/builder/ref-proxy.ts | 14 +++++++++++--- packages/db/tests/query/builder/ref-proxy.test.ts | 8 ++++++-- packages/db/tests/query/scheduler.test.ts | 7 ++++++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index eed5af0aa..033beac51 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -16,7 +16,11 @@ import { QueryMustHaveFromClauseError, SubQueryMustHaveFromClauseError, } from '../../errors.js' -import { checkCallbackForJsOperators, createRefProxy, toExpression } from './ref-proxy.js' +import { + checkCallbackForJsOperators, + createRefProxy, + toExpression, +} from './ref-proxy.js' import type { NamespacedRow, SingleResult } from '../../types.js' import type { Aggregate, diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 5a0d34161..31b44b9aa 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -8,11 +8,17 @@ import type { RefLeaf } from './types.js' * JavaScript tries to coerce a RefProxy to a primitive value. * This catches misuse like string concatenation, arithmetic, etc. */ -function createToPrimitiveHandler(path: Array): (hint: string) => never { +function createToPrimitiveHandler( + path: Array, +): (hint: string) => never { return (hint: string) => { const pathStr = path.length > 0 ? path.join(`.`) : `` throw new JavaScriptOperatorInQueryError( - hint === `number` ? `arithmetic` : hint === `string` ? `string concatenation` : `comparison`, + hint === `number` + ? `arithmetic` + : hint === `string` + ? `string concatenation` + : `comparison`, `Attempted to use "${pathStr}" in a JavaScript ${hint} context.\n` + `Query references can only be used with query functions, not JavaScript operators.`, ) @@ -306,7 +312,9 @@ function stripStringsAndComments(source: string): string { * // This is fine: * checkCallbackForJsOperators(({users}) => users.data) */ -export function checkCallbackForJsOperators(callback: (...args: Array) => unknown): void { +export function checkCallbackForJsOperators( + callback: (...args: Array) => unknown, +): void { const source = callback.toString() // Strip strings and comments to avoid false positives diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index 0c15016c1..050472125 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -278,12 +278,16 @@ describe(`ref-proxy`, () => { describe(`Symbol.toPrimitive trap`, () => { it(`throws error when proxy is coerced to string`, () => { const proxy = createRefProxy<{ users: { id: number } }>([`users`]) - expect(() => String(proxy.users.id)).toThrow(JavaScriptOperatorInQueryError) + expect(() => String(proxy.users.id)).toThrow( + JavaScriptOperatorInQueryError, + ) }) it(`throws error when proxy is used in arithmetic`, () => { const proxy = createRefProxy<{ users: { id: number } }>([`users`]) - expect(() => Number(proxy.users.id)).toThrow(JavaScriptOperatorInQueryError) + expect(() => Number(proxy.users.id)).toThrow( + JavaScriptOperatorInQueryError, + ) }) it(`throws error when proxy is concatenated with string`, () => { diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index 2de85931e..d53798486 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -1,6 +1,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { coalesce, createLiveQueryCollection, eq, isNull } from '../../src/query/index.js' +import { + coalesce, + createLiveQueryCollection, + eq, + isNull, +} from '../../src/query/index.js' import { createTransaction } from '../../src/transactions.js' import { createOptimisticAction } from '../../src/optimistic-action.js' import { transactionScopedScheduler } from '../../src/scheduler.js' From 05ea0100e19a365d678c008d12c418384dc7077b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Dec 2025 14:19:52 +0000 Subject: [PATCH 03/13] fix(db): fix TypeScript type error in checkCallbackForJsOperators --- packages/db/src/query/builder/ref-proxy.ts | 4 ++-- packages/db/tests/query/live-query-collection.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 31b44b9aa..498cdd2f1 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -312,8 +312,8 @@ function stripStringsAndComments(source: string): string { * // This is fine: * checkCallbackForJsOperators(({users}) => users.data) */ -export function checkCallbackForJsOperators( - callback: (...args: Array) => unknown, +export function checkCallbackForJsOperators) => any>( + callback: T, ): void { const source = callback.toString() diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index df286760b..bc2c8bbeb 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -1911,9 +1911,9 @@ describe(`createLiveQueryCollection`, () => { .join({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id)) .select(({ comments: c, users: u }) => ({ id: c.id, - userId: u.id, + userId: u!.id, text: c.text, - userName: u.name, + userName: u!.name, })), getKey: (item) => item.userId, startSync: true, From 7c6c5377192fd99f94afd5a342b9f24409ccf2a5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:21:48 +0000 Subject: [PATCH 04/13] ci: apply automated fixes --- packages/db/src/query/builder/ref-proxy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 498cdd2f1..318de0943 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -312,9 +312,9 @@ function stripStringsAndComments(source: string): string { * // This is fine: * checkCallbackForJsOperators(({users}) => users.data) */ -export function checkCallbackForJsOperators) => any>( - callback: T, -): void { +export function checkCallbackForJsOperators< + T extends (...args: Array) => any, +>(callback: T): void { const source = callback.toString() // Strip strings and comments to avoid false positives From 0dfb603316e5ff9458fc279777a27d183b3564b4 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 6 Jan 2026 09:43:50 -0700 Subject: [PATCH 05/13] Add ternary operator detection and fix e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ternary operator (?:) detection to JS operator patterns - Add tests for ternary detection and regex literal edge case - Fix e2e test to use coalesce() instead of ?? - Add changeset 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/prevent-js-operators-in-queries.md | 13 ++++++++ .../src/suites/predicates.suite.ts | 3 +- packages/db/src/query/builder/ref-proxy.ts | 7 +++++ .../db/tests/query/builder/ref-proxy.test.ts | 31 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/prevent-js-operators-in-queries.md diff --git a/.changeset/prevent-js-operators-in-queries.md b/.changeset/prevent-js-operators-in-queries.md new file mode 100644 index 000000000..1a440942c --- /dev/null +++ b/.changeset/prevent-js-operators-in-queries.md @@ -0,0 +1,13 @@ +--- +"@tanstack/db": patch +--- + +Add detection and error messages for JavaScript operators in query callbacks + +This adds helpful error messages when users mistakenly use JavaScript operators (`||`, `&&`, `??`, `?:`) in query callbacks. These operators are evaluated at query construction time rather than execution time, causing silent unexpected behavior. + +Changes: +- Add `JavaScriptOperatorInQueryError` with helpful suggestions for alternatives +- Add `Symbol.toPrimitive` trap to `RefProxy` to catch primitive coercion attempts +- Add `checkCallbackForJsOperators()` to detect operators in callback source code +- Integrate checks into `select()`, `where()`, and `having()` methods diff --git a/packages/db-collection-e2e/src/suites/predicates.suite.ts b/packages/db-collection-e2e/src/suites/predicates.suite.ts index 0ea3a9947..4d7a4a00c 100644 --- a/packages/db-collection-e2e/src/suites/predicates.suite.ts +++ b/packages/db-collection-e2e/src/suites/predicates.suite.ts @@ -8,6 +8,7 @@ import { describe, expect, it } from 'vitest' import { and, + coalesce, createLiveQueryCollection, eq, gt, @@ -472,7 +473,7 @@ export function createPredicatesTestSuite( .where(({ post }) => or( like(lower(post.title), `%${searchLower}%`), - like(lower(post.content ?? ``), `%${searchLower}%`), + like(lower(coalesce(post.content, ``)), `%${searchLower}%`), ), ), ) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 318de0943..f5210ff21 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -278,6 +278,13 @@ const JS_OPERATOR_PATTERNS: Array<{ operator: `??`, description: `nullish coalescing`, }, + { + // Match ternary operator - looks for ? followed by : with something in between + // but not ?. (optional chaining) or ?? (nullish coalescing) + pattern: /\?[^.?][^:]*:/, + operator: `?:`, + description: `ternary`, + }, ] /** diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index 050472125..8e3f78409 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -242,6 +242,16 @@ describe(`ref-proxy`, () => { expect(() => checkCallbackForJsOperators(callback)).toThrow(`??`) }) + it(`throws error for ternary operator`, () => { + const callback = ({ users }: any) => ({ + status: users.active ? `active` : `inactive`, + }) + expect(() => checkCallbackForJsOperators(callback)).toThrow( + JavaScriptOperatorInQueryError, + ) + expect(() => checkCallbackForJsOperators(callback)).toThrow(`?:`) + }) + it(`does not throw for valid query callbacks`, () => { // Simple property access expect(() => @@ -272,6 +282,27 @@ describe(`ref-proxy`, () => { expect(() => checkCallbackForJsOperators(() => ({ message: `a && b is valid` })), ).not.toThrow() + + // ?: in a string literal should not trigger error + expect(() => + checkCallbackForJsOperators(() => ({ message: `a ? b : c is valid` })), + ).not.toThrow() + }) + + it(`does not throw for optional chaining`, () => { + // Optional chaining should not be confused with ternary + expect(() => + checkCallbackForJsOperators(({ users }: any) => users?.name), + ).not.toThrow() + }) + + it(`throws for operators in regex literals (known limitation)`, () => { + // This is a known limitation - regex literals containing operators + // will trigger false positives. Document the behavior. + const callbackWithRegexOr = () => ({ pattern: /a||b/ }) + expect(() => checkCallbackForJsOperators(callbackWithRegexOr)).toThrow( + JavaScriptOperatorInQueryError, + ) }) }) From bd823a148e394d6d2e99332fe17aa8af44a0f917 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 16:44:49 +0000 Subject: [PATCH 06/13] ci: apply automated fixes --- .changeset/prevent-js-operators-in-queries.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/prevent-js-operators-in-queries.md b/.changeset/prevent-js-operators-in-queries.md index 1a440942c..f9eaf0c5f 100644 --- a/.changeset/prevent-js-operators-in-queries.md +++ b/.changeset/prevent-js-operators-in-queries.md @@ -1,5 +1,5 @@ --- -"@tanstack/db": patch +'@tanstack/db': patch --- Add detection and error messages for JavaScript operators in query callbacks @@ -7,6 +7,7 @@ Add detection and error messages for JavaScript operators in query callbacks This adds helpful error messages when users mistakenly use JavaScript operators (`||`, `&&`, `??`, `?:`) in query callbacks. These operators are evaluated at query construction time rather than execution time, causing silent unexpected behavior. Changes: + - Add `JavaScriptOperatorInQueryError` with helpful suggestions for alternatives - Add `Symbol.toPrimitive` trap to `RefProxy` to catch primitive coercion attempts - Add `checkCallbackForJsOperators()` to detect operators in callback source code From 7b639d37466dd51de78bb600dfb6f8d9e09d109c Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 7 Jan 2026 08:49:40 -0700 Subject: [PATCH 07/13] Fix type error in e2e test coalesce usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cast coalesce result to string to satisfy lower() type requirement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/db-collection-e2e/src/suites/predicates.suite.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/db-collection-e2e/src/suites/predicates.suite.ts b/packages/db-collection-e2e/src/suites/predicates.suite.ts index 4d7a4a00c..b956aa806 100644 --- a/packages/db-collection-e2e/src/suites/predicates.suite.ts +++ b/packages/db-collection-e2e/src/suites/predicates.suite.ts @@ -473,7 +473,10 @@ export function createPredicatesTestSuite( .where(({ post }) => or( like(lower(post.title), `%${searchLower}%`), - like(lower(coalesce(post.content, ``)), `%${searchLower}%`), + like( + lower(coalesce(post.content, ``) as unknown as string), + `%${searchLower}%`, + ), ), ), ) From 3f71f8e2b165e63bda59d3926328fcda2ff9702a Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 7 Jan 2026 08:52:18 -0700 Subject: [PATCH 08/13] Simplify e2e test - use non-null assertion instead of coalesce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LOWER(NULL) LIKE '%x%' is NULL (falsy) in SQL, so coalesce isn't needed when using OR with a title match that will find results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/db-collection-e2e/src/suites/predicates.suite.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/db-collection-e2e/src/suites/predicates.suite.ts b/packages/db-collection-e2e/src/suites/predicates.suite.ts index b956aa806..1aaf7ce91 100644 --- a/packages/db-collection-e2e/src/suites/predicates.suite.ts +++ b/packages/db-collection-e2e/src/suites/predicates.suite.ts @@ -8,7 +8,6 @@ import { describe, expect, it } from 'vitest' import { and, - coalesce, createLiveQueryCollection, eq, gt, @@ -473,10 +472,7 @@ export function createPredicatesTestSuite( .where(({ post }) => or( like(lower(post.title), `%${searchLower}%`), - like( - lower(coalesce(post.content, ``) as unknown as string), - `%${searchLower}%`, - ), + like(lower(post.content!), `%${searchLower}%`), ), ), ) From 03e01beeb3e20847ee8881739f42aae9aa2864f2 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 20 Jan 2026 09:20:30 -0700 Subject: [PATCH 09/13] Change JS operator detection to warn-only in dev mode - Add missing Symbol.toPrimitive trap to createSelectedProxy for consistency - Change checkCallbackForJsOperators to warn instead of throw (avoids false positive blocking) - Only run regex-based detection in development mode (NODE_ENV !== 'production') - Remove misleading comments on JS_OPERATOR_PATTERNS - Update tests to verify warning behavior and production mode skip Co-Authored-By: Claude Opus 4.5 --- packages/db/src/query/builder/ref-proxy.ts | 45 +++++-- .../db/tests/query/builder/ref-proxy.test.ts | 119 ++++++++++-------- 2 files changed, 98 insertions(+), 66 deletions(-) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 0b40bd38d..3814f57e1 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -239,6 +239,10 @@ export function createRefProxyWithSelected>( if (prop === `__refProxy`) return true if (prop === `__path`) return [`$selected`, ...path] if (prop === `__type`) return undefined + // Intercept Symbol.toPrimitive to catch JS coercion attempts + if (prop === Symbol.toPrimitive) { + return createToPrimitiveHandler([`$selected`, ...path]) + } if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver) const newPath = [...path, String(prop)] @@ -350,26 +354,22 @@ const JS_OPERATOR_PATTERNS: Array<{ description: string }> = [ { - // Match || that's not inside a string or comment - // This regex looks for || not preceded by quotes that would indicate a string pattern: /\|\|/, operator: `||`, description: `logical OR`, }, { - // Match && that's not inside a string or comment pattern: /&&/, operator: `&&`, description: `logical AND`, }, { - // Match ?? nullish coalescing pattern: /\?\?/, operator: `??`, description: `nullish coalescing`, }, { - // Match ternary operator - looks for ? followed by : with something in between + // Matches ? followed by : with something in between, // but not ?. (optional chaining) or ?? (nullish coalescing) pattern: /\?[^.?][^:]*:/, operator: `?:`, @@ -399,11 +399,14 @@ function stripStringsAndComments(source: string): string { * Checks a callback function's source code for JavaScript operators that * cannot be translated to query operations. * + * Only runs in development mode (NODE_ENV !== 'production') and logs a warning + * instead of throwing, since regex-based detection can have false positives + * (e.g., operators inside regex literals). + * * @param callback - The callback function to check - * @throws JavaScriptOperatorInQueryError if a problematic operator is found * * @example - * // This will throw an error: + * // This will log a warning in dev: * checkCallbackForJsOperators(({users}) => users.data || []) * * // This is fine: @@ -412,6 +415,11 @@ function stripStringsAndComments(source: string): string { export function checkCallbackForJsOperators< T extends (...args: Array) => any, >(callback: T): void { + // Only run in development mode + if (process.env.NODE_ENV === `production`) { + return + } + const source = callback.toString() // Strip strings and comments to avoid false positives @@ -419,12 +427,27 @@ export function checkCallbackForJsOperators< for (const { pattern, operator, description } of JS_OPERATOR_PATTERNS) { if (pattern.test(cleanedSource)) { - throw new JavaScriptOperatorInQueryError( - operator, - `Found JavaScript ${description} operator (${operator}) in query callback.\n` + + const hint = + operator === `||` || operator === `??` + ? `Use coalesce() instead: coalesce(value, defaultValue)` + : operator === `&&` + ? `Use and() for logical conditions` + : operator === `?:` + ? `Use cond() for conditional expressions: cond(condition, trueValue, falseValue)` + : `Use the appropriate query function instead` + + console.warn( + `[TanStack DB] JavaScript operator "${operator}" detected in query callback.\n\n` + + `Found JavaScript ${description} operator (${operator}) in query callback.\n` + `This operator is evaluated at query construction time, not at query execution time,\n` + - `which means it will not behave as expected.`, + `which means it will not behave as expected.\n\n` + + `${hint}\n\n` + + `Example of incorrect usage:\n` + + ` .select(({users}) => ({ data: users.data || [] }))\n\n` + + `Correct usage:\n` + + ` .select(({users}) => ({ data: coalesce(users.data, []) }))`, ) + return // Only warn once per callback } } } diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index 8e3f78409..a59fa6a00 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { checkCallbackForJsOperators, createRefProxy, @@ -218,91 +218,100 @@ describe(`ref-proxy`, () => { }) describe(`checkCallbackForJsOperators`, () => { - it(`throws error for || operator`, () => { + let warnSpy: ReturnType + const originalEnv = process.env.NODE_ENV + + beforeEach(() => { + warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) + // Ensure we're in dev mode for tests + process.env.NODE_ENV = `development` + }) + + afterEach(() => { + warnSpy.mockRestore() + process.env.NODE_ENV = originalEnv + }) + + it(`warns for || operator`, () => { const callback = ({ users }: any) => ({ data: users.data || [] }) - expect(() => checkCallbackForJsOperators(callback)).toThrow( - JavaScriptOperatorInQueryError, - ) - expect(() => checkCallbackForJsOperators(callback)).toThrow(`||`) + checkCallbackForJsOperators(callback) + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy.mock.calls[0][0]).toContain(`||`) }) - it(`throws error for && operator`, () => { + it(`warns for && operator`, () => { const callback = ({ users }: any) => users.active && users.name - expect(() => checkCallbackForJsOperators(callback)).toThrow( - JavaScriptOperatorInQueryError, - ) - expect(() => checkCallbackForJsOperators(callback)).toThrow(`&&`) + checkCallbackForJsOperators(callback) + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy.mock.calls[0][0]).toContain(`&&`) }) - it(`throws error for ?? operator`, () => { + it(`warns for ?? operator`, () => { const callback = ({ users }: any) => ({ name: users.name ?? `default` }) - expect(() => checkCallbackForJsOperators(callback)).toThrow( - JavaScriptOperatorInQueryError, - ) - expect(() => checkCallbackForJsOperators(callback)).toThrow(`??`) + checkCallbackForJsOperators(callback) + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy.mock.calls[0][0]).toContain(`??`) }) - it(`throws error for ternary operator`, () => { + it(`warns for ternary operator`, () => { const callback = ({ users }: any) => ({ status: users.active ? `active` : `inactive`, }) - expect(() => checkCallbackForJsOperators(callback)).toThrow( - JavaScriptOperatorInQueryError, - ) - expect(() => checkCallbackForJsOperators(callback)).toThrow(`?:`) + checkCallbackForJsOperators(callback) + expect(warnSpy).toHaveBeenCalledTimes(1) + expect(warnSpy.mock.calls[0][0]).toContain(`?:`) }) - it(`does not throw for valid query callbacks`, () => { + it(`does not warn for valid query callbacks`, () => { // Simple property access - expect(() => - checkCallbackForJsOperators(({ users }: any) => users.name), - ).not.toThrow() + checkCallbackForJsOperators(({ users }: any) => users.name) + expect(warnSpy).not.toHaveBeenCalled() // Object with property access - expect(() => - checkCallbackForJsOperators(({ users }: any) => ({ - id: users.id, - name: users.name, - })), - ).not.toThrow() + checkCallbackForJsOperators(({ users }: any) => ({ + id: users.id, + name: users.name, + })) + expect(warnSpy).not.toHaveBeenCalled() // Optional chaining is allowed - expect(() => - checkCallbackForJsOperators(({ users }: any) => users.profile?.bio), - ).not.toThrow() + checkCallbackForJsOperators(({ users }: any) => users.profile?.bio) + expect(warnSpy).not.toHaveBeenCalled() }) - it(`does not throw for operators in string literals`, () => { - // || in a string literal should not trigger error - expect(() => - checkCallbackForJsOperators(() => ({ message: `a || b is valid` })), - ).not.toThrow() + it(`does not warn for operators in string literals`, () => { + // || in a string literal should not trigger warning + checkCallbackForJsOperators(() => ({ message: `a || b is valid` })) + expect(warnSpy).not.toHaveBeenCalled() - // && in a string literal should not trigger error - expect(() => - checkCallbackForJsOperators(() => ({ message: `a && b is valid` })), - ).not.toThrow() + // && in a string literal should not trigger warning + checkCallbackForJsOperators(() => ({ message: `a && b is valid` })) + expect(warnSpy).not.toHaveBeenCalled() - // ?: in a string literal should not trigger error - expect(() => - checkCallbackForJsOperators(() => ({ message: `a ? b : c is valid` })), - ).not.toThrow() + // ?: in a string literal should not trigger warning + checkCallbackForJsOperators(() => ({ message: `a ? b : c is valid` })) + expect(warnSpy).not.toHaveBeenCalled() }) - it(`does not throw for optional chaining`, () => { + it(`does not warn for optional chaining`, () => { // Optional chaining should not be confused with ternary - expect(() => - checkCallbackForJsOperators(({ users }: any) => users?.name), - ).not.toThrow() + checkCallbackForJsOperators(({ users }: any) => users?.name) + expect(warnSpy).not.toHaveBeenCalled() }) - it(`throws for operators in regex literals (known limitation)`, () => { + it(`warns for operators in regex literals (known limitation)`, () => { // This is a known limitation - regex literals containing operators // will trigger false positives. Document the behavior. const callbackWithRegexOr = () => ({ pattern: /a||b/ }) - expect(() => checkCallbackForJsOperators(callbackWithRegexOr)).toThrow( - JavaScriptOperatorInQueryError, - ) + checkCallbackForJsOperators(callbackWithRegexOr) + expect(warnSpy).toHaveBeenCalledTimes(1) + }) + + it(`does not warn in production mode`, () => { + process.env.NODE_ENV = `production` + const callback = ({ users }: any) => ({ data: users.data || [] }) + checkCallbackForJsOperators(callback) + expect(warnSpy).not.toHaveBeenCalled() }) }) From 970383f07634ab6d5baa7371b8de4f36f517e1da Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 20 Jan 2026 09:22:51 -0700 Subject: [PATCH 10/13] Update changeset to reflect warn-only behavior Co-Authored-By: Claude Opus 4.5 --- .changeset/prevent-js-operators-in-queries.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/prevent-js-operators-in-queries.md b/.changeset/prevent-js-operators-in-queries.md index f9eaf0c5f..4bacc7259 100644 --- a/.changeset/prevent-js-operators-in-queries.md +++ b/.changeset/prevent-js-operators-in-queries.md @@ -2,13 +2,13 @@ '@tanstack/db': patch --- -Add detection and error messages for JavaScript operators in query callbacks +Add development warnings for JavaScript operators in query callbacks -This adds helpful error messages when users mistakenly use JavaScript operators (`||`, `&&`, `??`, `?:`) in query callbacks. These operators are evaluated at query construction time rather than execution time, causing silent unexpected behavior. +Warns developers when they mistakenly use JavaScript operators (`||`, `&&`, `??`, `?:`) in query callbacks. These operators are evaluated at query construction time rather than execution time, causing silent unexpected behavior. Changes: -- Add `JavaScriptOperatorInQueryError` with helpful suggestions for alternatives -- Add `Symbol.toPrimitive` trap to `RefProxy` to catch primitive coercion attempts -- Add `checkCallbackForJsOperators()` to detect operators in callback source code +- Add `Symbol.toPrimitive` trap to RefProxy to catch primitive coercion (throws error) +- Add `checkCallbackForJsOperators()` to detect operators in callback source code (warns in dev only) - Integrate checks into `select()`, `where()`, and `having()` methods +- Detection is disabled in production mode for zero runtime overhead From 1733471f4556e3b1c37e87d239a0c6514856f906 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 20 Jan 2026 09:26:54 -0700 Subject: [PATCH 11/13] Fix TypeScript errors in test assertions Add non-null assertions for mock call access after verifying call count. Co-Authored-By: Claude Opus 4.5 --- packages/db/tests/query/builder/ref-proxy.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/db/tests/query/builder/ref-proxy.test.ts b/packages/db/tests/query/builder/ref-proxy.test.ts index a59fa6a00..f916d9c38 100644 --- a/packages/db/tests/query/builder/ref-proxy.test.ts +++ b/packages/db/tests/query/builder/ref-proxy.test.ts @@ -236,21 +236,21 @@ describe(`ref-proxy`, () => { const callback = ({ users }: any) => ({ data: users.data || [] }) checkCallbackForJsOperators(callback) expect(warnSpy).toHaveBeenCalledTimes(1) - expect(warnSpy.mock.calls[0][0]).toContain(`||`) + expect(warnSpy.mock.calls[0]![0]).toContain(`||`) }) it(`warns for && operator`, () => { const callback = ({ users }: any) => users.active && users.name checkCallbackForJsOperators(callback) expect(warnSpy).toHaveBeenCalledTimes(1) - expect(warnSpy.mock.calls[0][0]).toContain(`&&`) + expect(warnSpy.mock.calls[0]![0]).toContain(`&&`) }) it(`warns for ?? operator`, () => { const callback = ({ users }: any) => ({ name: users.name ?? `default` }) checkCallbackForJsOperators(callback) expect(warnSpy).toHaveBeenCalledTimes(1) - expect(warnSpy.mock.calls[0][0]).toContain(`??`) + expect(warnSpy.mock.calls[0]![0]).toContain(`??`) }) it(`warns for ternary operator`, () => { @@ -259,7 +259,7 @@ describe(`ref-proxy`, () => { }) checkCallbackForJsOperators(callback) expect(warnSpy).toHaveBeenCalledTimes(1) - expect(warnSpy.mock.calls[0][0]).toContain(`?:`) + expect(warnSpy.mock.calls[0]![0]).toContain(`?:`) }) it(`does not warn for valid query callbacks`, () => { From 22d6289b2d485fb395e00ed93d7a10644de582c6 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 20 Jan 2026 09:34:02 -0700 Subject: [PATCH 12/13] Simplify nested ternaries with switch statements - Extract getOperatorTypeFromHint() for Symbol.toPrimitive error messages - Extract getHintForOperator() for warning hint messages - Use method chaining in stripStringsAndComments() Co-Authored-By: Claude Opus 4.5 --- packages/db/src/query/builder/ref-proxy.ts | 58 ++++++++++++---------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 3814f57e1..934b54058 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -8,17 +8,24 @@ import type { RefLeaf } from './types.js' * JavaScript tries to coerce a RefProxy to a primitive value. * This catches misuse like string concatenation, arithmetic, etc. */ +function getOperatorTypeFromHint(hint: string): string { + switch (hint) { + case `number`: + return `arithmetic` + case `string`: + return `string concatenation` + default: + return `comparison` + } +} + function createToPrimitiveHandler( path: Array, ): (hint: string) => never { return (hint: string) => { const pathStr = path.length > 0 ? path.join(`.`) : `` throw new JavaScriptOperatorInQueryError( - hint === `number` - ? `arithmetic` - : hint === `string` - ? `string concatenation` - : `comparison`, + getOperatorTypeFromHint(hint), `Attempted to use "${pathStr}" in a JavaScript ${hint} context.\n` + `Query references can only be used with query functions, not JavaScript operators.`, ) @@ -377,22 +384,31 @@ const JS_OPERATOR_PATTERNS: Array<{ }, ] +function getHintForOperator(operator: string): string { + switch (operator) { + case `||`: + case `??`: + return `Use coalesce() instead: coalesce(value, defaultValue)` + case `&&`: + return `Use and() for logical conditions` + case `?:`: + return `Use cond() for conditional expressions: cond(condition, trueValue, falseValue)` + default: + return `Use the appropriate query function instead` + } +} + /** * Removes string literals and comments from source code to avoid false positives * when checking for JavaScript operators. */ function stripStringsAndComments(source: string): string { - // Remove template literals (backtick strings) - let result = source.replace(/`(?:[^`\\]|\\.)*`/g, `""`) - // Remove double-quoted strings - result = result.replace(/"(?:[^"\\]|\\.)*"/g, `""`) - // Remove single-quoted strings - result = result.replace(/'(?:[^'\\]|\\.)*'/g, `""`) - // Remove single-line comments - result = result.replace(/\/\/[^\n]*/g, ``) - // Remove multi-line comments - result = result.replace(/\/\*[\s\S]*?\*\//g, ``) - return result + return source + .replace(/`(?:[^`\\]|\\.)*`/g, `""`) // template literals + .replace(/"(?:[^"\\]|\\.)*"/g, `""`) // double-quoted strings + .replace(/'(?:[^'\\]|\\.)*'/g, `""`) // single-quoted strings + .replace(/\/\/[^\n]*/g, ``) // single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ``) // multi-line comments } /** @@ -427,15 +443,7 @@ export function checkCallbackForJsOperators< for (const { pattern, operator, description } of JS_OPERATOR_PATTERNS) { if (pattern.test(cleanedSource)) { - const hint = - operator === `||` || operator === `??` - ? `Use coalesce() instead: coalesce(value, defaultValue)` - : operator === `&&` - ? `Use and() for logical conditions` - : operator === `?:` - ? `Use cond() for conditional expressions: cond(condition, trueValue, falseValue)` - : `Use the appropriate query function instead` - + const hint = getHintForOperator(operator) console.warn( `[TanStack DB] JavaScript operator "${operator}" detected in query callback.\n\n` + `Found JavaScript ${description} operator (${operator}) in query callback.\n` + From 1b4f2c6c1c2614358c6362033c141fea71ed85ec Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 20 Jan 2026 09:40:14 -0700 Subject: [PATCH 13/13] Move dev-only code inside condition for dead code elimination All JS operator detection logic (patterns, helpers, string stripping) is now inside the `if (process.env.NODE_ENV !== 'production')` block. This allows bundlers to completely eliminate this code from production builds. Co-Authored-By: Claude Opus 4.5 --- packages/db/src/query/builder/ref-proxy.ts | 140 ++++++++------------- 1 file changed, 55 insertions(+), 85 deletions(-) diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 934b54058..96a2c6519 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -350,67 +350,6 @@ export function val(value: T): BasicExpression { return new Value(value) } -/** - * Patterns that indicate JavaScript operators being used in query callbacks. - * These operators cannot be translated to query operations and will silently - * produce incorrect results. - */ -const JS_OPERATOR_PATTERNS: Array<{ - pattern: RegExp - operator: string - description: string -}> = [ - { - pattern: /\|\|/, - operator: `||`, - description: `logical OR`, - }, - { - pattern: /&&/, - operator: `&&`, - description: `logical AND`, - }, - { - pattern: /\?\?/, - operator: `??`, - description: `nullish coalescing`, - }, - { - // Matches ? followed by : with something in between, - // but not ?. (optional chaining) or ?? (nullish coalescing) - pattern: /\?[^.?][^:]*:/, - operator: `?:`, - description: `ternary`, - }, -] - -function getHintForOperator(operator: string): string { - switch (operator) { - case `||`: - case `??`: - return `Use coalesce() instead: coalesce(value, defaultValue)` - case `&&`: - return `Use and() for logical conditions` - case `?:`: - return `Use cond() for conditional expressions: cond(condition, trueValue, falseValue)` - default: - return `Use the appropriate query function instead` - } -} - -/** - * Removes string literals and comments from source code to avoid false positives - * when checking for JavaScript operators. - */ -function stripStringsAndComments(source: string): string { - return source - .replace(/`(?:[^`\\]|\\.)*`/g, `""`) // template literals - .replace(/"(?:[^"\\]|\\.)*"/g, `""`) // double-quoted strings - .replace(/'(?:[^'\\]|\\.)*'/g, `""`) // single-quoted strings - .replace(/\/\/[^\n]*/g, ``) // single-line comments - .replace(/\/\*[\s\S]*?\*\//g, ``) // multi-line comments -} - /** * Checks a callback function's source code for JavaScript operators that * cannot be translated to query operations. @@ -419,6 +358,9 @@ function stripStringsAndComments(source: string): string { * instead of throwing, since regex-based detection can have false positives * (e.g., operators inside regex literals). * + * All detection logic is inside the dev check so bundlers can eliminate it + * entirely from production builds. + * * @param callback - The callback function to check * * @example @@ -431,31 +373,59 @@ function stripStringsAndComments(source: string): string { export function checkCallbackForJsOperators< T extends (...args: Array) => any, >(callback: T): void { - // Only run in development mode - if (process.env.NODE_ENV === `production`) { - return - } + if (process.env.NODE_ENV !== `production`) { + // Patterns that indicate JavaScript operators being used in query callbacks + const JS_OPERATOR_PATTERNS = [ + { pattern: /\|\|/, operator: `||`, description: `logical OR` }, + { pattern: /&&/, operator: `&&`, description: `logical AND` }, + { pattern: /\?\?/, operator: `??`, description: `nullish coalescing` }, + { + // Matches ? followed by : with something in between, + // but not ?. (optional chaining) or ?? (nullish coalescing) + pattern: /\?[^.?][^:]*:/, + operator: `?:`, + description: `ternary`, + }, + ] + + const getHintForOperator = (operator: string): string => { + switch (operator) { + case `||`: + case `??`: + return `Use coalesce() instead: coalesce(value, defaultValue)` + case `&&`: + return `Use and() for logical conditions` + case `?:`: + return `Use cond() for conditional expressions: cond(condition, trueValue, falseValue)` + default: + return `Use the appropriate query function instead` + } + } - const source = callback.toString() - - // Strip strings and comments to avoid false positives - const cleanedSource = stripStringsAndComments(source) - - for (const { pattern, operator, description } of JS_OPERATOR_PATTERNS) { - if (pattern.test(cleanedSource)) { - const hint = getHintForOperator(operator) - console.warn( - `[TanStack DB] JavaScript operator "${operator}" detected in query callback.\n\n` + - `Found JavaScript ${description} operator (${operator}) in query callback.\n` + - `This operator is evaluated at query construction time, not at query execution time,\n` + - `which means it will not behave as expected.\n\n` + - `${hint}\n\n` + - `Example of incorrect usage:\n` + - ` .select(({users}) => ({ data: users.data || [] }))\n\n` + - `Correct usage:\n` + - ` .select(({users}) => ({ data: coalesce(users.data, []) }))`, - ) - return // Only warn once per callback + // Strip string literals and comments to avoid false positives + const cleanedSource = callback + .toString() + .replace(/`(?:[^`\\]|\\.)*`/g, `""`) // template literals + .replace(/"(?:[^"\\]|\\.)*"/g, `""`) // double-quoted strings + .replace(/'(?:[^'\\]|\\.)*'/g, `""`) // single-quoted strings + .replace(/\/\/[^\n]*/g, ``) // single-line comments + .replace(/\/\*[\s\S]*?\*\//g, ``) // multi-line comments + + for (const { pattern, operator, description } of JS_OPERATOR_PATTERNS) { + if (pattern.test(cleanedSource)) { + console.warn( + `[TanStack DB] JavaScript operator "${operator}" detected in query callback.\n\n` + + `Found JavaScript ${description} operator (${operator}) in query callback.\n` + + `This operator is evaluated at query construction time, not at query execution time,\n` + + `which means it will not behave as expected.\n\n` + + `${getHintForOperator(operator)}\n\n` + + `Example of incorrect usage:\n` + + ` .select(({users}) => ({ data: users.data || [] }))\n\n` + + `Correct usage:\n` + + ` .select(({users}) => ({ data: coalesce(users.data, []) }))`, + ) + return // Only warn once per callback + } } } }