From 4cb347497a4c4f850d36113d9637c4c3b3c17994 Mon Sep 17 00:00:00 2001 From: Lucas Duailibe Date: Fri, 26 Sep 2025 11:34:15 -0300 Subject: [PATCH 1/3] skip join optimization when joining with a computed value --- packages/db/src/query/compiler/joins.ts | 7 ++++++- packages/db/tests/query/join.test.ts | 26 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/db/src/query/compiler/joins.ts b/packages/db/src/query/compiler/joins.ts index 42687e446..24498a0be 100644 --- a/packages/db/src/query/compiler/joins.ts +++ b/packages/db/src/query/compiler/joins.ts @@ -204,7 +204,12 @@ function processJoin( lazyFrom.type === `queryRef` && (lazyFrom.query.limit || lazyFrom.query.offset) - if (!limitedSubquery) { + // If join expressions are computed values (like concat functions), we can't optimize + // the join because followRef expects PropRef objects but gets Func objects instead + const hasComputedJoinExpr = + mainExpr.type === `func` || joinedExpr.type === `func` + + if (!limitedSubquery && !hasComputedJoinExpr) { // This join can be optimized by having the active collection // dynamically load keys into the lazy collection // based on the value of the joinKey and by looking up diff --git a/packages/db/tests/query/join.test.ts b/packages/db/tests/query/join.test.ts index 9b2d3ef24..dc0eca84f 100644 --- a/packages/db/tests/query/join.test.ts +++ b/packages/db/tests/query/join.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from "vitest" -import { createLiveQueryCollection, eq } from "../../src/query/index.js" +import { concat, createLiveQueryCollection, eq } from "../../src/query/index.js" import { createCollection } from "../../src/collection/index.js" import { mockSyncCollectionOptions } from "../utils.js" @@ -948,6 +948,30 @@ function createJoinTests(autoIndex: `off` | `eager`): void { const alice = results.find((r) => r.user_name === `Alice`) expect(alice?.department_name).toBe(`Engineering`) }) + + test(`should handle joins with computed expressions`, () => { + const joinQuery = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .join( + { dept: departmentsCollection }, + ({ user, dept }) => + eq( + concat(`dept`, user.department_id), + concat(`dept`, dept.id) + ), + `inner` + ) + .select(({ user, dept }) => ({ + user_name: user.name, + department_name: dept.name, + })), + }) + + expect(joinQuery.size).toBe(3) + }) }) test(`should handle chained joins with incremental updates`, () => { From 4022ddb30467ef91650633cdc5b142205417ecde Mon Sep 17 00:00:00 2001 From: Lucas Duailibe Date: Fri, 26 Sep 2025 12:04:15 -0300 Subject: [PATCH 2/3] changeset --- .changeset/five-waves-rule.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/five-waves-rule.md diff --git a/.changeset/five-waves-rule.md b/.changeset/five-waves-rule.md new file mode 100644 index 000000000..020f9bbba --- /dev/null +++ b/.changeset/five-waves-rule.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +Fix joins using conditions with computed values (such as `concat()`) From d1e9970b3f36ff7ca77bd3f484b2827904816416 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Mon, 29 Sep 2025 14:37:32 +0200 Subject: [PATCH 3/3] Rephrased comment --- packages/db/src/query/compiler/joins.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/src/query/compiler/joins.ts b/packages/db/src/query/compiler/joins.ts index 24498a0be..50326339e 100644 --- a/packages/db/src/query/compiler/joins.ts +++ b/packages/db/src/query/compiler/joins.ts @@ -204,8 +204,8 @@ function processJoin( lazyFrom.type === `queryRef` && (lazyFrom.query.limit || lazyFrom.query.offset) - // If join expressions are computed values (like concat functions), we can't optimize - // the join because followRef expects PropRef objects but gets Func objects instead + // If join expressions contain computed values (like concat functions) + // we don't optimize the join because we don't have an index over the computed values const hasComputedJoinExpr = mainExpr.type === `func` || joinedExpr.type === `func`