From 32271c4ac41c97f549a355a2a3f156f50ce21f8f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 23:25:51 +0000 Subject: [PATCH 1/2] fix(db): add validation for where() expressions to catch JavaScript operator usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When users accidentally use JavaScript's comparison operators (===, !==, <, >, etc.) in where() callbacks instead of query builder functions (eq, gt, etc.), the query builder now throws a helpful InvalidWhereExpressionError with clear guidance. Previously, this mistake would result in a confusing "Unknown expression type: undefined" error at query compilation time. Now users get immediate feedback with an example of the correct syntax. Example of the mistake this catches: ❌ .where(({ user }) => user.id === 'abc') ✅ .where(({ user }) => eq(user.id, 'abc')) --- packages/db/src/errors.ts | 13 +++++ packages/db/src/query/builder/index.ts | 31 +++++++++++ packages/db/tests/query/builder/where.test.ts | 54 +++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 50cd442ac..b6d8b385a 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -390,6 +390,19 @@ export class QueryMustHaveFromClauseError extends QueryBuilderError { } } +export class InvalidWhereExpressionError extends QueryBuilderError { + constructor(valueType: string) { + super( + `Invalid where() expression: Expected a query expression, but received a ${valueType}. ` + + `This usually happens when using JavaScript's comparison operators (===, !==, <, >, etc.) directly. ` + + `Instead, use the query builder functions:\n\n` + + ` ❌ .where(({ user }) => user.id === 'abc')\n` + + ` ✅ .where(({ user }) => eq(user.id, 'abc'))\n\n` + + `Available comparison functions: eq, gt, gte, lt, lte, and, or, not, like, ilike, isNull, isUndefined`, + ) + } +} + // Query Compilation Errors export class QueryCompilationError extends TanStackDBError { constructor(message: string) { diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index e7d2be0c4..e7f445291 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -11,6 +11,7 @@ import { import { InvalidSourceError, InvalidSourceTypeError, + InvalidWhereExpressionError, JoinConditionMustBeEqualityError, OnlyOneSourceAllowedError, QueryMustHaveFromClauseError, @@ -361,6 +362,21 @@ export class BaseQueryBuilder { const refProxy = createRefProxy(aliases) as RefsForContext const expression = callback(refProxy) + // Validate that the callback returned a valid expression + // This catches common mistakes like using JavaScript comparison operators (===, !==, etc.) + // which return boolean primitives instead of expression objects + if (!isExpressionLike(expression)) { + const valueType = + expression === null + ? `null` + : expression === undefined + ? `undefined` + : typeof expression === `object` + ? `object` + : typeof expression + throw new InvalidWhereExpressionError(valueType) + } + const existingWhere = this.query.where || [] return new BaseQueryBuilder({ @@ -402,6 +418,21 @@ export class BaseQueryBuilder { const refProxy = createRefProxy(aliases) as RefsForContext const expression = callback(refProxy) + // Validate that the callback returned a valid expression + // This catches common mistakes like using JavaScript comparison operators (===, !==, etc.) + // which return boolean primitives instead of expression objects + if (!isExpressionLike(expression)) { + const valueType = + expression === null + ? `null` + : expression === undefined + ? `undefined` + : typeof expression === `object` + ? `object` + : typeof expression + throw new InvalidWhereExpressionError(valueType) + } + const existingHaving = this.query.having || [] return new BaseQueryBuilder({ diff --git a/packages/db/tests/query/builder/where.test.ts b/packages/db/tests/query/builder/where.test.ts index e6aebb3af..78036d953 100644 --- a/packages/db/tests/query/builder/where.test.ts +++ b/packages/db/tests/query/builder/where.test.ts @@ -13,6 +13,7 @@ import { not, or, } from '../../../src/query/builder/functions.js' +import { InvalidWhereExpressionError } from '../../../src/errors.js' // Test schema interface Employee { @@ -185,4 +186,57 @@ describe(`QueryBuilder.where`, () => { expect((builtQuery.where as any)[0]?.name).toBe(`eq`) expect((builtQuery.where as any)[1]?.name).toBe(`gt`) }) + + describe(`error handling`, () => { + it(`throws InvalidWhereExpressionError when using JavaScript === operator`, () => { + const builder = new Query() + expect(() => + builder + .from({ employees: employeesCollection }) + // This is a common mistake - using JavaScript's === instead of eq() + .where(({ employees }) => (employees.id as any) === 1), + ).toThrow(InvalidWhereExpressionError) + }) + + it(`throws InvalidWhereExpressionError when callback returns a boolean`, () => { + const builder = new Query() + expect(() => + builder + .from({ employees: employeesCollection }) + .where(() => true as any), + ).toThrow(InvalidWhereExpressionError) + }) + + it(`throws InvalidWhereExpressionError when callback returns undefined`, () => { + const builder = new Query() + expect(() => + builder + .from({ employees: employeesCollection }) + .where(() => undefined as any), + ).toThrow(InvalidWhereExpressionError) + }) + + it(`throws InvalidWhereExpressionError when callback returns null`, () => { + const builder = new Query() + expect(() => + builder + .from({ employees: employeesCollection }) + .where(() => null as any), + ).toThrow(InvalidWhereExpressionError) + }) + + it(`throws InvalidWhereExpressionError with helpful message mentioning eq()`, () => { + const builder = new Query() + try { + builder + .from({ employees: employeesCollection }) + .where(({ employees }) => (employees.id as any) === 1) + expect.fail(`Expected error to be thrown`) + } catch (e) { + expect(e).toBeInstanceOf(InvalidWhereExpressionError) + expect((e as Error).message).toContain(`eq(`) + expect((e as Error).message).toContain(`===`) + } + }) + }) }) From e5cb7801caafb7e03017479d1313625f51b78ca5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 23:30:58 +0000 Subject: [PATCH 2/2] chore: add changeset for where() expression validation --- .changeset/fix-invalid-where-expression.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/fix-invalid-where-expression.md diff --git a/.changeset/fix-invalid-where-expression.md b/.changeset/fix-invalid-where-expression.md new file mode 100644 index 000000000..5ae372f7d --- /dev/null +++ b/.changeset/fix-invalid-where-expression.md @@ -0,0 +1,14 @@ +--- +'@tanstack/db': patch +--- + +Add validation for where() and having() expressions to catch JavaScript operator usage + +When users accidentally use JavaScript's comparison operators (`===`, `!==`, `<`, `>`, etc.) in `where()` or `having()` callbacks instead of query builder functions (`eq`, `gt`, etc.), the query builder now throws a helpful `InvalidWhereExpressionError` with clear guidance. + +Previously, this mistake would result in a confusing "Unknown expression type: undefined" error at query compilation time. Now users get immediate feedback with an example of the correct syntax: + +``` +❌ .where(({ user }) => user.id === 'abc') +✅ .where(({ user }) => eq(user.id, 'abc')) +```