Skip to content
Draft
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
119 changes: 76 additions & 43 deletions packages/db/src/query/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '../ir.js'
import { ensureIndexForField } from '../../indexes/auto-index.js'
import { inArray } from '../builder/functions.js'
import { extractEqualityKeys } from '../expression-helpers.js'
import { compileExpression, toBooleanPredicate } from './evaluators.js'
import { processJoins } from './joins.js'
import { containsAggregate, processGroupBy } from './group-by.js'
Expand All @@ -38,6 +39,7 @@ import type {
IncludesMaterialization,
QueryIR,
QueryRef,
Where,
} from '../ir.js'
import type { LazyCollectionCallbacks } from './joins.js'
import type { Collection } from '../../collection/index.js'
Expand Down Expand Up @@ -396,67 +398,98 @@ export function compileQuery(
childFromCollection,
)

// When the parent query statically constrains the correlation field to
// known keys (e.g. `.where(eq(parent.id, 5))`), the child subset can be
// loaded eagerly β€” in parallel with the parent β€” instead of being
// deferred behind the parent pipeline. Deferring it makes the child
// loadSubset miss a progressive collection's fast-path window.
const parentCorrelationAlias = subquery.correlationField.path[0]
const staticCorrelationKeys = parentCorrelationAlias
? extractEqualityKeys(
sourceWhereClauses.get(parentCorrelationAlias),
subquery.correlationField.path,
)
: null
// Non-empty key set => load the child eagerly; otherwise lazy-load it.
const eagerChildKeys =
staticCorrelationKeys && staticCorrelationKeys.length > 0
? staticCorrelationKeys
: null

if (followRefResult) {
const followRefCollection = followRefResult.collection
const fieldPath = followRefResult.path
const fieldName = fieldPath[0]

// 1. Mark child source as lazy so CollectionSubscriber skips initial full load
lazySources.add(childCorrelationAlias)

// 2. Ensure an index on the correlation field for efficient lookups
// Ensure an index on the correlation field for efficient lookups
if (fieldName) {
ensureIndexForField(fieldName, fieldPath, followRefCollection)
}

// 3. Tap parent keys to intercept correlation values and request
// matching child rows on-demand via the child's subscription
parentKeys = parentKeys.pipe(
tap((data: any) => {
const resolvedAlias =
aliasRemapping[childCorrelationAlias] || childCorrelationAlias
const lazySourceSubscription = subscriptions[resolvedAlias]
// When the keys aren't statically known, fall back to lazy loading:
// mark the child source lazy so its CollectionSubscriber skips the
// initial full load, and tap the parent keys to request matching
// child rows on-demand once the parent pipeline produces them.
if (!eagerChildKeys) {
lazySources.add(childCorrelationAlias)

parentKeys = parentKeys.pipe(
tap((data: any) => {
const resolvedAlias =
aliasRemapping[childCorrelationAlias] || childCorrelationAlias
const lazySourceSubscription = subscriptions[resolvedAlias]

if (!lazySourceSubscription) {
return
}

if (!lazySourceSubscription) {
return
}
if (lazySourceSubscription.hasLoadedInitialState()) {
return
}

if (lazySourceSubscription.hasLoadedInitialState()) {
return
}
const joinKeys = [
...new Set(
data
.getInner()
.map(
([[correlationValue]]: any) =>
correlationValue as unknown,
)
.filter((joinKey: unknown) => joinKey != null),
),
]

if (joinKeys.length === 0) {
return
}

const joinKeys = [
...new Set(
data
.getInner()
.map(
([[correlationValue]]: any) => correlationValue as unknown,
)
.filter((key: unknown) => key != null),
),
]

if (joinKeys.length === 0) {
return
}
const lazyJoinRef = new PropRef(fieldPath)
lazySourceSubscription.requestSnapshot({
where: inArray(lazyJoinRef, joinKeys),
})
}),
)
}
}

const lazyJoinRef = new PropRef(fieldPath)
lazySourceSubscription.requestSnapshot({
where: inArray(lazyJoinRef, joinKeys),
})
}),
// Extra WHERE clauses appended to the child query: parent-referencing
// filters (applied post-join) and, when the parent keys are statically
// known, an eager correlation predicate so the child loads its subset
// immediately via the normal (non-lazy) subscription path.
const extraChildWhere: Array<Where> = []
if (subquery.parentFilters && subquery.parentFilters.length > 0) {
extraChildWhere.push(...subquery.parentFilters)
}
if (eagerChildKeys) {
extraChildWhere.push(
inArray(subquery.childCorrelationField, eagerChildKeys),
)
}

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

Expand Down
77 changes: 75 additions & 2 deletions packages/db/src/query/compiler/joins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
import { normalizeValue } from '../../utils/comparison.js'
import { ensureIndexForField } from '../../indexes/auto-index.js'
import { PropRef, followRef } from '../ir.js'
import { inArray } from '../builder/functions.js'
import { and, inArray } from '../builder/functions.js'
import { extractEqualityKeys } from '../expression-helpers.js'
import { compileExpression } from './evaluators.js'
import type { CompileQueryFn } from './index.js'
import type { OrderByOptimizationInfo } from './order-by.js'
Expand Down Expand Up @@ -221,7 +222,35 @@ function processJoin(
throw new UnsupportedJoinTypeError(joinClause.type)
}

if (activeSource) {
// When the query statically constrains the main side of the join condition
// to known keys, filter the joined side to the same keys so it loads only
// the matching subset eagerly. This avoids deferring it behind a
// lazy-loading tap, which would miss a progressive collection's fast-path
// window. Sound only for inner/left joins (see deriveStaticJoinFilter).
const canFilterJoinedSide =
joinClause.type === `inner` || joinClause.type === `left`
const staticJoinFilter = canFilterJoinedSide
? deriveStaticJoinFilter(
mainExpr,
joinedExpr,
joinedSource,
isCollectionRef,
sourceWhereClauses,
)
: null
if (staticJoinFilter) {
const existing = sourceWhereClauses.get(staticJoinFilter.alias)
sourceWhereClauses.set(
staticJoinFilter.alias,
existing
? and(existing, staticJoinFilter.filter)
: staticJoinFilter.filter,
)
}

// Skip lazy loading when the joined side is already statically filtered β€”
// it now loads its subset eagerly via the normal subscription path.
if (activeSource && !staticJoinFilter) {
// If the lazy collection comes from a subquery that has a limit and/or an offset clause
// then we need to deoptimize the join because we don't know which rows are in the result set
// since we simply lookup matching keys in the index but the index contains all rows
Expand Down Expand Up @@ -660,3 +689,47 @@ function getActiveAndLazySources(
return { activeSource: undefined, lazySource: undefined }
}
}

/**
* When the query statically constrains the main side of an inner/left join to
* a known set of keys (e.g. `.where(eq(group.id, 5))`), derives an equality
* filter for the joined side of the join condition.
*
* Applying that filter lets the joined collection load only the matching
* subset eagerly, instead of being deferred behind a lazy-loading tap (which
* misses a progressive collection's fast-path window) or loaded in full as the
* join's "active" side.
*
* Only sound for `inner` and `left` joins, where a joined row is needed solely
* when it matches a main row β€” `right`/`full` joins keep unmatched joined rows.
* Only plain collection join sources are filtered; subquery sources keep the
* existing lazy-loading behaviour.
*
* @returns The joined alias and the derived predicate, or `null` when the main
* side is not statically constrained.
*/
function deriveStaticJoinFilter(
mainExpr: BasicExpression,
joinedExpr: BasicExpression,
joinedSource: string,
joinedSideIsCollection: boolean,
sourceWhereClauses: Map<string, BasicExpression<boolean>>,
): { alias: string; filter: BasicExpression<boolean> } | null {
if (
!joinedSideIsCollection ||
mainExpr.type !== `ref` ||
joinedExpr.type !== `ref`
) {
return null
}

const mainKeys = extractEqualityKeys(
sourceWhereClauses.get(mainExpr.path[0]!),
mainExpr.path,
)
if (mainKeys && mainKeys.length > 0) {
return { alias: joinedSource, filter: inArray(joinedExpr, mainKeys) }
}

return null
}
75 changes: 75 additions & 0 deletions packages/db/src/query/expression-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,78 @@ export function parseLoadSubsetOptions(
limit: options.limit,
}
}

/**
* Determines whether a WHERE expression constrains the field at `fieldPath` to
* a fixed, statically-known set of literal values.
*
* Recognises a top-level `eq` or `in` comparison against the field β€” possibly
* nested inside `and` β€” and returns the literal value set. Returns `null` when
* the field is not statically constrained (for example it is compared against
* another column, or the predicate uses `or`).
*
* Used to push correlation predicates eagerly onto lazily-loaded join/includes
* children: when the parent side is filtered to known keys, the child subset
* can be loaded immediately instead of waiting for the parent query to resolve.
*
* @param where - The WHERE expression to inspect
* @param fieldPath - The full path of the field (alias included, e.g. `['user', 'id']`)
* @returns The literal value set, or `null` when not statically constrained
*
* @example
* ```typescript
* // eq(user.id, 5) -> [5]
* // in(user.id, [1, 2, 3]) -> [1, 2, 3]
* // and(eq(user.id, 5), ...) -> [5]
* // or(...) / eq(user.id, x) -> null
* ```
*/
export function extractEqualityKeys(
where: BasicExpression<boolean> | undefined,
fieldPath: ReadonlyArray<string>,
): Array<unknown> | null {
if (!where || where.type !== `func`) {
return null
}

const matchesField = (expr: BasicExpression): boolean =>
expr.type === `ref` &&
expr.path.length === fieldPath.length &&
expr.path.every((segment, index) => segment === fieldPath[index])

const { name, args } = where

if (name === `and`) {
for (const arg of args) {
const keys = extractEqualityKeys(
arg as BasicExpression<boolean>,
fieldPath,
)
if (keys) {
return keys
}
}
return null
}

if (name === `eq` && args.length === 2) {
const [lhs, rhs] = args as [BasicExpression, BasicExpression]
if (matchesField(lhs) && rhs.type === `val`) {
return [rhs.value]
}
if (matchesField(rhs) && lhs.type === `val`) {
return [lhs.value]
}
return null
}

if (name === `in` && args.length === 2) {
const [lhs, rhs] = args as [BasicExpression, BasicExpression]
if (matchesField(lhs) && rhs.type === `val` && Array.isArray(rhs.value)) {
return [...rhs.value]
}
return null
}

return null
}
Loading