Skip to content

Commit 4ee665a

Browse files
committed
better mapping of subquery aliases
1 parent 04688eb commit 4ee665a

File tree

3 files changed

+68
-39
lines changed

3 files changed

+68
-39
lines changed

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface CompilationResult {
4242
sourceWhereClauses: Map<string, BasicExpression<boolean>>
4343
/** Map of alias to underlying collection id used during compilation */
4444
aliasToCollectionId: Record<string, string>
45+
/** Map of outer alias to inner alias for subquery aliasing (e.g., 'activeUser' → 'user') */
46+
aliasRemapping: Record<string, string>
4547
}
4648

4749
/**
@@ -88,6 +90,11 @@ export function compileQuery(
8890
// the live layer can subscribe to every alias the optimizer introduces.
8991
const aliasToCollectionId: Record<string, string> = {}
9092

93+
// Track alias remapping for subqueries (outer alias → inner alias)
94+
// e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })
95+
// we store: aliasRemapping['activeUser'] = 'user'
96+
const aliasRemapping: Record<string, string> = {}
97+
9198
// Create a map of source aliases to input streams.
9299
// Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),
93100
// not by collection ID. This enables per-alias subscriptions where different aliases
@@ -109,7 +116,8 @@ export function compileQuery(
109116
optimizableOrderByCollections,
110117
cache,
111118
queryMapping,
112-
aliasToCollectionId
119+
aliasToCollectionId,
120+
aliasRemapping
113121
)
114122
sources[mainSource] = mainInput
115123

@@ -143,7 +151,8 @@ export function compileQuery(
143151
optimizableOrderByCollections,
144152
rawQuery,
145153
compileQuery,
146-
aliasToCollectionId
154+
aliasToCollectionId,
155+
aliasRemapping
147156
)
148157
}
149158

@@ -303,6 +312,7 @@ export function compileQuery(
303312
pipeline: result,
304313
sourceWhereClauses,
305314
aliasToCollectionId,
315+
aliasRemapping,
306316
}
307317
cache.set(rawQuery, compilationResult)
308318

@@ -332,6 +342,7 @@ export function compileQuery(
332342
pipeline: result,
333343
sourceWhereClauses,
334344
aliasToCollectionId,
345+
aliasRemapping,
335346
}
336347
cache.set(rawQuery, compilationResult)
337348

@@ -351,7 +362,8 @@ function processFrom(
351362
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
352363
cache: QueryCache,
353364
queryMapping: QueryMapping,
354-
aliasToCollectionId: Record<string, string>
365+
aliasToCollectionId: Record<string, string>,
366+
aliasRemapping: Record<string, string>
355367
): { alias: string; input: KeyedStream; collectionId: string } {
356368
switch (from.type) {
357369
case `collectionRef`: {
@@ -383,7 +395,20 @@ function processFrom(
383395
queryMapping
384396
)
385397

398+
// Pull up the inner alias mappings
386399
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)
400+
Object.assign(aliasRemapping, subQueryResult.aliasRemapping)
401+
402+
// For subqueries, the outer alias (from.alias) may differ from inner aliases.
403+
// Find the inner alias that corresponds to the subquery's main collection and create a remapping.
404+
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
405+
(alias) =>
406+
subQueryResult.aliasToCollectionId[alias] ===
407+
subQueryResult.collectionId
408+
)
409+
if (innerAlias && innerAlias !== from.alias) {
410+
aliasRemapping[from.alias] = innerAlias
411+
}
387412

388413
// Extract the pipeline from the compilation result
389414
const subQueryInput = subQueryResult.pipeline

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

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ export function processJoins(
6464
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
6565
rawQuery: QueryIR,
6666
onCompileSubquery: CompileQueryFn,
67-
aliasToCollectionId: Record<string, string>
67+
aliasToCollectionId: Record<string, string>,
68+
aliasRemapping: Record<string, string>
6869
): NamespacedAndKeyedStream {
6970
let resultPipeline = pipeline
7071

@@ -85,7 +86,8 @@ export function processJoins(
8586
optimizableOrderByCollections,
8687
rawQuery,
8788
onCompileSubquery,
88-
aliasToCollectionId
89+
aliasToCollectionId,
90+
aliasRemapping
8991
)
9092
}
9193

@@ -111,7 +113,8 @@ function processJoin(
111113
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
112114
rawQuery: QueryIR,
113115
onCompileSubquery: CompileQueryFn,
114-
aliasToCollectionId: Record<string, string>
116+
aliasToCollectionId: Record<string, string>,
117+
aliasRemapping: Record<string, string>
115118
): NamespacedAndKeyedStream {
116119
const isCollectionRef = joinClause.from.type === `collectionRef`
117120

@@ -131,7 +134,8 @@ function processJoin(
131134
cache,
132135
queryMapping,
133136
onCompileSubquery,
134-
aliasToCollectionId
137+
aliasToCollectionId,
138+
aliasRemapping
135139
)
136140

137141
// Add the joined source to the sources map
@@ -270,23 +274,18 @@ function processJoin(
270274
const lazyAliasCandidate =
271275
activeSource === `main` ? joinedSource : mainSource
272276

273-
// The alias candidate might be a subquery alias without a direct subscription.
274-
// In that case, find an alias from aliasToCollectionId that maps to the lazy collection.
275-
let lazySourceSubscription = subscriptions[lazyAliasCandidate]
276-
if (!lazySourceSubscription) {
277-
// Search for any alias that maps to the lazy collection ID
278-
const matchingAlias = Object.entries(aliasToCollectionId).find(
279-
([_alias, collId]) => collId === lazySource.id
280-
)?.[0]
281-
282-
if (matchingAlias) {
283-
lazySourceSubscription = subscriptions[matchingAlias]
284-
}
285-
}
277+
// Find the subscription for lazy loading.
278+
// For subqueries, the outer join alias (e.g., 'activeUser') may differ from the
279+
// inner alias (e.g., 'user'). Use aliasRemapping to resolve outer → inner alias.
280+
// Example: .join({ activeUser: subquery }) where subquery uses .from({ user: collection })
281+
// → aliasRemapping['activeUser'] = 'user'
282+
const resolvedAlias =
283+
aliasRemapping[lazyAliasCandidate] || lazyAliasCandidate
284+
const lazySourceSubscription = subscriptions[resolvedAlias]
286285

287286
if (!lazySourceSubscription) {
288287
throw new Error(
289-
`Internal error: subscription for alias '${lazyAliasCandidate}' (collection '${lazySource.id}') is missing in join pipeline. Available aliases: ${Object.keys(subscriptions).join(`, `)}. This indicates a bug in alias tracking.`
288+
`Internal error: subscription for alias '${resolvedAlias}' (remapped from '${lazyAliasCandidate}', collection '${lazySource.id}') is missing in join pipeline. Available aliases: ${Object.keys(subscriptions).join(`, `)}. This indicates a bug in alias tracking.`
290289
)
291290
}
292291

@@ -427,7 +426,8 @@ function processJoinSource(
427426
cache: QueryCache,
428427
queryMapping: QueryMapping,
429428
onCompileSubquery: CompileQueryFn,
430-
aliasToCollectionId: Record<string, string>
429+
aliasToCollectionId: Record<string, string>,
430+
aliasRemapping: Record<string, string>
431431
): { alias: string; input: KeyedStream; collectionId: string } {
432432
switch (from.type) {
433433
case `collectionRef`: {
@@ -459,9 +459,20 @@ function processJoinSource(
459459
queryMapping
460460
)
461461

462-
// Pull the nested alias map up so the caller can subscribe to those aliases
463-
// and keep the current alias pointing at the subquery's collection.
462+
// Pull up the inner alias mappings
464463
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)
464+
Object.assign(aliasRemapping, subQueryResult.aliasRemapping)
465+
466+
// For subqueries, the outer alias (from.alias) may differ from inner aliases.
467+
// Find the inner alias that corresponds to the subquery's main collection and create a remapping.
468+
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
469+
(alias) =>
470+
subQueryResult.aliasToCollectionId[alias] ===
471+
subQueryResult.collectionId
472+
)
473+
if (innerAlias && innerAlias !== from.alias) {
474+
aliasRemapping[from.alias] = innerAlias
475+
}
465476

466477
// Extract the pipeline from the compilation result
467478
const subQueryInput = subQueryResult.pipeline

packages/db/tests/query/compiler/subqueries.test.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,11 @@ describe(`Query2 Subqueries`, () => {
273273

274274
const usersSubscription = usersCollection.subscribeChanges(() => {})
275275
const issuesSubscription = issuesCollection.subscribeChanges(() => {})
276+
277+
// Create subscriptions keyed by alias (matching production behavior)
276278
const subscriptions: Record<string, CollectionSubscription> = {
277-
[usersCollection.id]: usersSubscription,
278-
[issuesCollection.id]: issuesSubscription,
279+
issue: issuesSubscription,
280+
user: usersSubscription,
279281
}
280282

281283
// Compile and execute the query
@@ -297,17 +299,6 @@ describe(`Query2 Subqueries`, () => {
297299
)
298300
const { pipeline } = compilation
299301

300-
for (const [alias, collectionId] of Object.entries(
301-
compilation.aliasToCollectionId
302-
)) {
303-
if (!subscriptions[alias]) {
304-
subscriptions[alias] =
305-
collectionId === usersCollection.id
306-
? usersSubscription
307-
: issuesSubscription
308-
}
309-
}
310-
311302
// Since we're doing a left join, the alias on the right (from the subquery) should be handled lazily
312303
// The subquery uses 'user' alias, but the join uses 'activeUser' - we expect the lazy alias
313304
// to be the one that's marked (which is 'activeUser' since it's the joinedTableAlias)
@@ -377,9 +368,11 @@ describe(`Query2 Subqueries`, () => {
377368

378369
const usersSubscription = usersCollection.subscribeChanges(() => {})
379370
const issuesSubscription = issuesCollection.subscribeChanges(() => {})
371+
372+
// Create subscriptions keyed by alias (matching production behavior)
380373
const subscriptions: Record<string, CollectionSubscription> = {
381-
[usersCollection.id]: usersSubscription,
382-
[issuesCollection.id]: issuesSubscription,
374+
issue: issuesSubscription,
375+
user: usersSubscription,
383376
}
384377

385378
const dummyCallbacks = {

0 commit comments

Comments
 (0)