Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/includes-parent-referencing-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

feat: support parent-referencing WHERE filters in includes child queries
151 changes: 142 additions & 9 deletions packages/db/src/query/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
OrderBy,
OrderByDirection,
QueryIR,
Where,
} from '../ir.js'
import type {
CompareOptions,
Expand Down Expand Up @@ -871,6 +872,39 @@ function buildNestedSelect(obj: any, parentAliases: Array<string> = []): any {
return out
}

/**
* Recursively collects all PropRef nodes from an expression tree.
*/
function collectRefsFromExpression(expr: BasicExpression): Array<PropRef> {
const refs: Array<PropRef> = []
switch (expr.type) {
case `ref`:
refs.push(expr)
break
case `func`:
for (const arg of (expr as any).args ?? []) {
refs.push(...collectRefsFromExpression(arg))
}
break
default:
break
}
return refs
}

/**
* Checks whether a WHERE clause references any parent alias.
*/
function referencesParent(where: Where, parentAliases: Array<string>): boolean {
const expr =
typeof where === `object` && `expression` in where
? where.expression
: where
return collectRefsFromExpression(expr).some(
(ref) => ref.path[0] != null && parentAliases.includes(ref.path[0]),
)
}

/**
* Builds an IncludesSubquery IR node from a child query builder.
* Extracts the correlation condition from the child's WHERE clauses by finding
Expand All @@ -891,10 +925,12 @@ function buildIncludesSubquery(
}
}

// Walk child's WHERE clauses to find the correlation condition
// Walk child's WHERE clauses to find the correlation condition.
// The correlation eq() may be a standalone WHERE or nested inside a top-level and().
let parentRef: PropRef | undefined
let childRef: PropRef | undefined
let correlationWhereIndex = -1
let correlationAndArgIndex = -1 // >= 0 when found inside an and()

if (childQuery.where) {
for (let i = 0; i < childQuery.where.length; i++) {
Expand All @@ -904,16 +940,15 @@ function buildIncludesSubquery(
? where.expression
: where

// Look for eq(a, b) where one side references parent and other references child
// Try standalone eq()
if (
expr.type === `func` &&
expr.name === `eq` &&
expr.args.length === 2
) {
const [argA, argB] = expr.args
const result = extractCorrelation(
argA!,
argB!,
expr.args[0]!,
expr.args[1]!,
parentAliases,
childAliases,
)
Expand All @@ -924,6 +959,37 @@ function buildIncludesSubquery(
break
}
}

// Try inside top-level and()
if (
expr.type === `func` &&
expr.name === `and` &&
expr.args.length >= 2
) {
for (let j = 0; j < expr.args.length; j++) {
const arg = expr.args[j]!
if (
arg.type === `func` &&
arg.name === `eq` &&
arg.args.length === 2
) {
const result = extractCorrelation(
arg.args[0]!,
arg.args[1]!,
parentAliases,
childAliases,
)
if (result) {
parentRef = result.parentRef
childRef = result.childRef
correlationWhereIndex = i
correlationAndArgIndex = j
break
}
}
}
if (parentRef) break
}
}
}

Expand All @@ -935,15 +1001,82 @@ function buildIncludesSubquery(
)
}

// Remove the correlation WHERE from the child query
// Remove the correlation eq() from the child query's WHERE clauses.
// If it was inside an and(), remove just that arg (collapsing the and() if needed).
const modifiedWhere = [...childQuery.where!]
modifiedWhere.splice(correlationWhereIndex, 1)
if (correlationAndArgIndex >= 0) {
const where = modifiedWhere[correlationWhereIndex]!
const expr =
typeof where === `object` && `expression` in where
? where.expression
: where
const remainingArgs = (expr as any).args.filter(
(_: any, idx: number) => idx !== correlationAndArgIndex,
)
if (remainingArgs.length === 1) {
// Collapse and() with single remaining arg to just that expression
const isResidual =
typeof where === `object` && `expression` in where && where.residual
modifiedWhere[correlationWhereIndex] = isResidual
? { expression: remainingArgs[0], residual: true }
: remainingArgs[0]
} else {
// Rebuild and() without the extracted arg
const newAnd = new FuncExpr(`and`, remainingArgs)
const isResidual =
typeof where === `object` && `expression` in where && where.residual
modifiedWhere[correlationWhereIndex] = isResidual
? { expression: newAnd, residual: true }
: newAnd
}
} else {
modifiedWhere.splice(correlationWhereIndex, 1)
}

// Separate remaining WHEREs into pure-child vs parent-referencing
const pureChildWhere: Array<Where> = []
const parentFilters: Array<Where> = []
for (const w of modifiedWhere) {
if (referencesParent(w, parentAliases)) {
parentFilters.push(w)
} else {
pureChildWhere.push(w)
}
}

// Collect distinct parent PropRefs from parent-referencing filters
let parentProjection: Array<PropRef> | undefined
if (parentFilters.length > 0) {
const seen = new Set<string>()
parentProjection = []
for (const w of parentFilters) {
const expr = typeof w === `object` && `expression` in w ? w.expression : w
for (const ref of collectRefsFromExpression(expr)) {
if (
ref.path[0] != null &&
parentAliases.includes(ref.path[0]) &&
!seen.has(ref.path.join(`.`))
) {
seen.add(ref.path.join(`.`))
parentProjection.push(ref)
}
}
}
}

const modifiedQuery: QueryIR = {
...childQuery,
where: modifiedWhere.length > 0 ? modifiedWhere : undefined,
where: pureChildWhere.length > 0 ? pureChildWhere : undefined,
}

return new IncludesSubquery(modifiedQuery, parentRef, childRef, fieldName)
return new IncludesSubquery(
modifiedQuery,
parentRef,
childRef,
fieldName,
parentFilters.length > 0 ? parentFilters : undefined,
parentProjection,
)
}

/**
Expand Down
78 changes: 66 additions & 12 deletions packages/db/src/query/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,20 @@ export function compileQuery(
// Inner join: only children whose correlation key exists in parent keys pass through
const joined = childRekeyed.pipe(joinOperator(parentKeyStream, `inner`))

// Extract: [correlationValue, [[childKey, childRow], null]] → [childKey, childRow]
// Extract: [correlationValue, [[childKey, childRow], parentContext]] → [childKey, childRow]
// Tag the row with __correlationKey for output routing
// If parentSide is non-null (parent context projected), attach as __parentContext
filteredMainInput = joined.pipe(
filter(([_correlationValue, [childSide]]: any) => {
return childSide != null
}),
map(([correlationValue, [childSide, _parentSide]]: any) => {
map(([correlationValue, [childSide, parentSide]]: any) => {
const [childKey, childRow] = childSide
return [childKey, { ...childRow, __correlationKey: correlationValue }]
const tagged: any = { ...childRow, __correlationKey: correlationValue }
if (parentSide != null) {
tagged.__parentContext = parentSide
}
return [childKey, tagged]
}),
)

Expand All @@ -220,10 +225,14 @@ export function compileQuery(
let pipeline: NamespacedAndKeyedStream = filteredMainInput.pipe(
map(([key, row]) => {
// Initialize the record with a nested structure
const ret = [key, { [mainSource]: row }] as [
string,
Record<string, typeof row>,
]
// If __parentContext exists (from parent-referencing includes), merge parent
// aliases into the namespaced row so WHERE can resolve parent refs
const { __parentContext, ...cleanRow } = row as any
const nsRow: Record<string, any> = { [mainSource]: cleanRow }
if (__parentContext) {
Object.assign(nsRow, __parentContext)
}
const ret = [key, nsRow] as [string, Record<string, typeof row>]
return ret
}),
)
Expand Down Expand Up @@ -285,15 +294,60 @@ export function compileQuery(
if (query.select) {
const includesEntries = extractIncludesFromSelect(query.select)
for (const { key, subquery } of includesEntries) {
// Branch parent pipeline: map to [correlationValue, null]
// Branch parent pipeline: map to [correlationValue, parentContext]
// When parentProjection exists, project referenced parent fields; otherwise null (zero overhead)
const compiledCorrelation = compileExpression(subquery.correlationField)
const parentKeys = pipeline.pipe(
map(([_key, nsRow]: any) => [compiledCorrelation(nsRow), null] as any),
)
let parentKeys: any
if (subquery.parentProjection && subquery.parentProjection.length > 0) {
const compiledProjections = subquery.parentProjection.map((ref) => ({
alias: ref.path[0]!,
field: ref.path.slice(1),
compiled: compileExpression(ref),
}))
parentKeys = pipeline.pipe(
map(([_key, nsRow]: any) => {
const parentContext: Record<string, Record<string, any>> = {}
for (const proj of compiledProjections) {
if (!parentContext[proj.alias]) {
parentContext[proj.alias] = {}
}
const value = proj.compiled(nsRow)
// Set nested field in the alias namespace
let target = parentContext[proj.alias]!
for (let i = 0; i < proj.field.length - 1; i++) {
if (!target[proj.field[i]!]) {
target[proj.field[i]!] = {}
}
target = target[proj.field[i]!]
}
target[proj.field[proj.field.length - 1]!] = value
}
return [compiledCorrelation(nsRow), parentContext] as any
}),
)
} else {
parentKeys = pipeline.pipe(
map(
([_key, nsRow]: any) => [compiledCorrelation(nsRow), null] as any,
),
)
}

// If parent filters exist, append them to the child query's WHERE
const childQuery =
subquery.parentFilters && subquery.parentFilters.length > 0
? {
...subquery.query,
where: [
...(subquery.query.where || []),
...subquery.parentFilters,
],
}
: subquery.query

// Recursively compile child query WITH the parent key stream
const childResult = compileQuery(
subquery.query,
childQuery,
allInputs,
collections,
subscriptions,
Expand Down
2 changes: 2 additions & 0 deletions packages/db/src/query/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export class IncludesSubquery extends BaseExpression {
public correlationField: PropRef, // Parent-side ref (e.g., project.id)
public childCorrelationField: PropRef, // Child-side ref (e.g., issue.projectId)
public fieldName: string, // Result field name (e.g., "issues")
public parentFilters?: Array<Where>, // WHERE clauses referencing parent aliases (applied post-join)
public parentProjection?: Array<PropRef>, // Parent field refs used by parentFilters
) {
super()
}
Expand Down
Loading
Loading