From 8a29874d9f8b7fe5a642ca3bda8c2a06ec58a454 Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Sun, 8 Mar 2026 18:35:27 +0000 Subject: [PATCH 1/3] fix(db): infer type in coalesce() instead of returning BasicExpression Previously coalesce() always returned BasicExpression, losing type information. Now it uses a generic to infer the non-nullable union of all argument types, matching the semantic of SQL COALESCE. Fixes #1341 --- .changeset/fix-coalesce-type.md | 5 +++++ packages/db/src/query/builder/functions.ts | 6 ++++-- packages/db/tests/query/builder/callback-types.test-d.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-coalesce-type.md diff --git a/.changeset/fix-coalesce-type.md b/.changeset/fix-coalesce-type.md new file mode 100644 index 000000000..c6e8fefd0 --- /dev/null +++ b/.changeset/fix-coalesce-type.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +fix(db): infer type in coalesce() instead of returning BasicExpression diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index 887c1468d..b11aea1fc 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -285,11 +285,13 @@ export function concat( ) } -export function coalesce(...args: Array): BasicExpression { +export function coalesce>( + ...args: T +): BasicExpression>> { return new Func( `coalesce`, args.map((arg) => toExpression(arg)), - ) + ) as BasicExpression>> } export function add( diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index 0e901b7fb..b1565ea32 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -150,7 +150,7 @@ describe(`Query Builder Callback Types`, () => { BasicExpression >() expectTypeOf(coalesce(user.name, `Unknown`)).toEqualTypeOf< - BasicExpression + BasicExpression >() return { From 15ef0f76e290bb7cd640d94b3a7bda8c09f8cd7f Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Tue, 17 Mar 2026 17:05:54 +0000 Subject: [PATCH 2/3] fix(db): preserve null in coalesce() when no guaranteed non-null arg - Use HasGuaranteedNonNull helper to track whether any arg statically cannot be null/undefined - Return CoalesceArgTypes | null unless a non-null arg guarantees it - Update tests to use spread args (varargs) instead of array - Update changeset description Addresses samwillis review feedback and kevin-dp ExpressionLike concerns --- .changeset/fix-coalesce-type.md | 4 ++- packages/db/src/query/builder/functions.ts | 28 +++++++++++++++++-- .../query/builder/callback-types.test-d.ts | 12 ++++++++ .../db/tests/query/builder/functions.test.ts | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.changeset/fix-coalesce-type.md b/.changeset/fix-coalesce-type.md index c6e8fefd0..b6e853148 100644 --- a/.changeset/fix-coalesce-type.md +++ b/.changeset/fix-coalesce-type.md @@ -2,4 +2,6 @@ "@tanstack/db": patch --- -fix(db): infer type in coalesce() instead of returning BasicExpression +fix(db): preserve null in coalesce() return type when no guaranteed non-null arg is present + +`coalesce()` was typed as returning `BasicExpression`, losing all type information. The signature now infers types from all arguments via tuple generics, returns the union of non-null arg types, and only removes nullability when at least one argument is statically guaranteed non-null. diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index b11aea1fc..434e53bef 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -285,13 +285,35 @@ export function concat( ) } -export function coalesce>( +// Helper type for coalesce: extracts non-nullish value types from all args +type CoalesceArgTypes> = { + [K in keyof T]: NonNullable> +}[number] + +// Whether any arg in the tuple is statically guaranteed non-null (i.e., does not include null | undefined) +type HasGuaranteedNonNull> = { + [K in keyof T]: null extends ExtractType + ? false + : undefined extends ExtractType + ? false + : true +}[number] extends false + ? false + : true + +// coalesce() return type: union of all non-null arg types; null included unless a guaranteed non-null arg exists +type CoalesceReturnType> = + HasGuaranteedNonNull extends true + ? BasicExpression> + : BasicExpression | null> + +export function coalesce]>( ...args: T -): BasicExpression>> { +): CoalesceReturnType { return new Func( `coalesce`, args.map((arg) => toExpression(arg)), - ) as BasicExpression>> + ) as CoalesceReturnType } export function add( diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index b1565ea32..16f2e2b4a 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -152,6 +152,18 @@ describe(`Query Builder Callback Types`, () => { expectTypeOf(coalesce(user.name, `Unknown`)).toEqualTypeOf< BasicExpression >() + // nullable-only: coalesce(nullable, nullable) → keeps null in return type + expectTypeOf( + coalesce(user.department_id, user.department_id), + ).toEqualTypeOf>() + // nullable + nullable literal null → keeps null + expectTypeOf(coalesce(user.department_id, null)).toEqualTypeOf< + BasicExpression + >() + // nullable + guaranteed non-null → strips null + expectTypeOf(coalesce(user.department_id, 0)).toEqualTypeOf< + BasicExpression + >() return { upper_name: upper(user.name), diff --git a/packages/db/tests/query/builder/functions.test.ts b/packages/db/tests/query/builder/functions.test.ts index fb6cb4f35..abc6391c9 100644 --- a/packages/db/tests/query/builder/functions.test.ts +++ b/packages/db/tests/query/builder/functions.test.ts @@ -196,7 +196,7 @@ describe(`QueryBuilder Functions`, () => { .from({ employees: employeesCollection }) .select(({ employees }) => ({ id: employees.id, - name_or_default: coalesce([employees.name, `Unknown`]), + name_or_default: coalesce(employees.name, `Unknown`), })) const builtQuery = getQueryIR(query) From 8ca84b62c0a8374ee7c725e9fc2315c805c296aa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:09:08 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- .changeset/fix-coalesce-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-coalesce-type.md b/.changeset/fix-coalesce-type.md index b6e853148..f52b10f2d 100644 --- a/.changeset/fix-coalesce-type.md +++ b/.changeset/fix-coalesce-type.md @@ -1,5 +1,5 @@ --- -"@tanstack/db": patch +'@tanstack/db': patch --- fix(db): preserve null in coalesce() return type when no guaranteed non-null arg is present