Skip to content

Commit 51954d8

Browse files
authored
fix chains joins (#511)
1 parent 08303e6 commit 51954d8

File tree

4 files changed

+405
-68
lines changed

4 files changed

+405
-68
lines changed

.changeset/seven-eggs-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
fix a bug that prevented chaining joins (joining collectionB to collectionA, then collectionC to collectionB) within one query without using a subquery

packages/db/src/errors.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -408,26 +408,33 @@ export class InvalidJoinConditionSameTableError extends JoinError {
408408
}
409409

410410
export class InvalidJoinConditionTableMismatchError extends JoinError {
411-
constructor(mainTableAlias: string, joinedTableAlias: string) {
411+
constructor() {
412+
super(`Invalid join condition: expressions must reference table aliases`)
413+
}
414+
}
415+
416+
export class InvalidJoinConditionLeftTableError extends JoinError {
417+
constructor(tableAlias: string) {
412418
super(
413-
`Invalid join condition: expressions must reference table aliases "${mainTableAlias}" and "${joinedTableAlias}"`
419+
`Invalid join condition: left expression refers to an unavailable table "${tableAlias}"`
414420
)
415421
}
416422
}
417423

418-
export class InvalidJoinConditionWrongTablesError extends JoinError {
419-
constructor(
420-
leftTableAlias: string,
421-
rightTableAlias: string,
422-
mainTableAlias: string,
423-
joinedTableAlias: string
424-
) {
424+
export class InvalidJoinConditionRightTableError extends JoinError {
425+
constructor(tableAlias: string) {
425426
super(
426-
`Invalid join condition: expressions reference tables "${leftTableAlias}" and "${rightTableAlias}" but join is between "${mainTableAlias}" and "${joinedTableAlias}"`
427+
`Invalid join condition: right expression does not refer to the joined table "${tableAlias}"`
427428
)
428429
}
429430
}
430431

432+
export class InvalidJoinCondition extends JoinError {
433+
constructor() {
434+
super(`Invalid join condition`)
435+
}
436+
}
437+
431438
export class UnsupportedJoinSourceTypeError extends JoinError {
432439
constructor(type: string) {
433440
super(`Unsupported join source type: ${type}`)

packages/db/src/query/compiler/joins.ts

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
} from "@tanstack/db-ivm"
88
import {
99
CollectionInputNotFoundError,
10+
InvalidJoinCondition,
11+
InvalidJoinConditionLeftTableError,
12+
InvalidJoinConditionRightTableError,
1013
InvalidJoinConditionSameTableError,
1114
InvalidJoinConditionTableMismatchError,
12-
InvalidJoinConditionWrongTablesError,
1315
JoinCollectionNotFoundError,
1416
UnsupportedJoinSourceTypeError,
1517
UnsupportedJoinTypeError,
@@ -139,10 +141,11 @@ function processJoin(
139141
)
140142

141143
// Analyze which table each expression refers to and swap if necessary
144+
const availableTableAliases = Object.keys(tables)
142145
const { mainExpr, joinedExpr } = analyzeJoinExpressions(
143146
joinClause.left,
144147
joinClause.right,
145-
mainTableAlias,
148+
availableTableAliases,
146149
joinedTableAlias
147150
)
148151

@@ -299,53 +302,65 @@ function processJoin(
299302

300303
/**
301304
* Analyzes join expressions to determine which refers to which table
302-
* and returns them in the correct order (main table expression first, joined table expression second)
305+
* and returns them in the correct order (available table expression first, joined table expression second)
303306
*/
304307
function analyzeJoinExpressions(
305308
left: BasicExpression,
306309
right: BasicExpression,
307-
mainTableAlias: string,
310+
allAvailableTableAliases: Array<string>,
308311
joinedTableAlias: string
309312
): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {
313+
// Filter out the joined table alias from the available table aliases
314+
const availableTableAliases = allAvailableTableAliases.filter(
315+
(alias) => alias !== joinedTableAlias
316+
)
317+
310318
const leftTableAlias = getTableAliasFromExpression(left)
311319
const rightTableAlias = getTableAliasFromExpression(right)
312320

313-
// If left expression refers to main table and right refers to joined table, keep as is
321+
// If left expression refers to an available table and right refers to joined table, keep as is
314322
if (
315-
leftTableAlias === mainTableAlias &&
323+
leftTableAlias &&
324+
availableTableAliases.includes(leftTableAlias) &&
316325
rightTableAlias === joinedTableAlias
317326
) {
318327
return { mainExpr: left, joinedExpr: right }
319328
}
320329

321-
// If left expression refers to joined table and right refers to main table, swap them
330+
// If left expression refers to joined table and right refers to an available table, swap them
322331
if (
323332
leftTableAlias === joinedTableAlias &&
324-
rightTableAlias === mainTableAlias
333+
rightTableAlias &&
334+
availableTableAliases.includes(rightTableAlias)
325335
) {
326336
return { mainExpr: right, joinedExpr: left }
327337
}
328338

339+
// If one expression doesn't refer to any table, this is an invalid join
340+
if (!leftTableAlias || !rightTableAlias) {
341+
// For backward compatibility, use the first available table alias in error message
342+
throw new InvalidJoinConditionTableMismatchError()
343+
}
344+
329345
// If both expressions refer to the same alias, this is an invalid join
330346
if (leftTableAlias === rightTableAlias) {
331-
throw new InvalidJoinConditionSameTableError(leftTableAlias || `unknown`)
347+
throw new InvalidJoinConditionSameTableError(leftTableAlias)
332348
}
333349

334-
// If one expression doesn't refer to either table, this is an invalid join
335-
if (!leftTableAlias || !rightTableAlias) {
336-
throw new InvalidJoinConditionTableMismatchError(
337-
mainTableAlias,
338-
joinedTableAlias
339-
)
350+
// Left side must refer to an available table
351+
// This cannot happen with the query builder as there is no way to build a ref
352+
// to an unavailable table, but just in case, but could happen with the IR
353+
if (!availableTableAliases.includes(leftTableAlias)) {
354+
throw new InvalidJoinConditionLeftTableError(leftTableAlias)
340355
}
341356

342-
// If expressions refer to tables not involved in this join, this is an invalid join
343-
throw new InvalidJoinConditionWrongTablesError(
344-
leftTableAlias,
345-
rightTableAlias,
346-
mainTableAlias,
347-
joinedTableAlias
348-
)
357+
// Right side must refer to the joined table
358+
if (rightTableAlias !== joinedTableAlias) {
359+
throw new InvalidJoinConditionRightTableError(joinedTableAlias)
360+
}
361+
362+
// This should not be reachable given the logic above, but just in case
363+
throw new InvalidJoinCondition()
349364
}
350365

351366
/**

0 commit comments

Comments
 (0)