@@ -38,6 +38,9 @@ import type { QueryCache, QueryMapping, WindowOptions } from './types.js'
3838
3939export type { WindowOptions } from './types.js'
4040
41+ /** Symbol used to tag parent $selected with routing metadata for includes */
42+ export const INCLUDES_ROUTING = Symbol ( `includesRouting` )
43+
4144/**
4245 * Result of compiling an includes subquery, including the child pipeline
4346 * and metadata needed to route child results to parent-scoped Collections.
@@ -55,6 +58,8 @@ export interface IncludesCompilationResult {
5558 hasOrderBy : boolean
5659 /** Full compilation result for the child query (for nested includes + alias tracking) */
5760 childCompilationResult : CompilationResult
61+ /** Parent-side projection refs for parent-referencing filters */
62+ parentProjection ?: Array < PropRef >
5863}
5964
6065/**
@@ -213,7 +218,11 @@ export function compileQuery(
213218 if ( parentSide != null ) {
214219 tagged . __parentContext = parentSide
215220 }
216- return [ childKey , tagged ]
221+ const effectiveKey =
222+ parentSide != null
223+ ? `${ String ( childKey ) } ::${ JSON . stringify ( parentSide ) } `
224+ : childKey
225+ return [ effectiveKey , tagged ]
217226 } ) ,
218227 )
219228
@@ -231,6 +240,7 @@ export function compileQuery(
231240 const nsRow : Record < string , any > = { [ mainSource ] : cleanRow }
232241 if ( __parentContext ) {
233242 Object . assign ( nsRow , __parentContext )
243+ ; ( nsRow as any ) . __parentContext = __parentContext
234244 }
235245 const ret = [ key , nsRow ] as [ string , Record < string , typeof row > ]
236246 return ret
@@ -291,6 +301,13 @@ export function compileQuery(
291301 // This must happen AFTER WHERE (so parent pipeline is filtered) but BEFORE processSelect
292302 // (so IncludesSubquery nodes are stripped before select compilation).
293303 const includesResults : Array < IncludesCompilationResult > = [ ]
304+ const includesRoutingFns : Array < {
305+ fieldName : string
306+ getRouting : ( nsRow : any ) => {
307+ correlationKey : unknown
308+ parentContext : Record < string , any > | null
309+ }
310+ } > = [ ]
294311 if ( query . select ) {
295312 const includesEntries = extractIncludesFromSelect ( query . select )
296313 for ( const { key, subquery } of includesEntries ) {
@@ -374,30 +391,52 @@ export function compileQuery(
374391 subquery . query . orderBy && subquery . query . orderBy . length > 0
375392 ) ,
376393 childCompilationResult : childResult ,
394+ parentProjection : subquery . parentProjection ,
377395 } )
378396
397+ // Capture routing function for INCLUDES_ROUTING tagging
398+ if ( subquery . parentProjection && subquery . parentProjection . length > 0 ) {
399+ const compiledProjs = subquery . parentProjection . map ( ( ref ) => ( {
400+ alias : ref . path [ 0 ] ! ,
401+ field : ref . path . slice ( 1 ) ,
402+ compiled : compileExpression ( ref ) ,
403+ } ) )
404+ const compiledCorr = compiledCorrelation
405+ includesRoutingFns . push ( {
406+ fieldName : subquery . fieldName ,
407+ getRouting : ( nsRow : any ) => {
408+ const parentContext : Record < string , Record < string , any > > = { }
409+ for ( const proj of compiledProjs ) {
410+ if ( ! parentContext [ proj . alias ] ) {
411+ parentContext [ proj . alias ] = { }
412+ }
413+ const value = proj . compiled ( nsRow )
414+ let target = parentContext [ proj . alias ] !
415+ for ( let i = 0 ; i < proj . field . length - 1 ; i ++ ) {
416+ if ( ! target [ proj . field [ i ] ! ] ) {
417+ target [ proj . field [ i ] ! ] = { }
418+ }
419+ target = target [ proj . field [ i ] ! ]
420+ }
421+ target [ proj . field [ proj . field . length - 1 ] ! ] = value
422+ }
423+ return { correlationKey : compiledCorr ( nsRow ) , parentContext }
424+ } ,
425+ } )
426+ } else {
427+ includesRoutingFns . push ( {
428+ fieldName : subquery . fieldName ,
429+ getRouting : ( nsRow : any ) => ( {
430+ correlationKey : compiledCorrelation ( nsRow ) ,
431+ parentContext : null ,
432+ } ) ,
433+ } )
434+ }
435+
379436 // Replace includes entry in select with a null placeholder
380437 replaceIncludesInSelect ( query . select , key )
381438 }
382439
383- // Stamp correlation key values onto the namespaced row so they survive
384- // select extraction. This allows flushIncludesState to read them directly
385- // without requiring the correlation field to be in the user's select.
386- if ( includesEntries . length > 0 ) {
387- const compiledCorrelations = includesEntries . map ( ( { subquery } ) => ( {
388- fieldName : subquery . fieldName ,
389- compiled : compileExpression ( subquery . correlationField ) ,
390- } ) )
391- pipeline = pipeline . pipe (
392- map ( ( [ key , nsRow ] : any ) => {
393- const correlationKeys : Record < string , unknown > = { }
394- for ( const { fieldName : fn , compiled } of compiledCorrelations ) {
395- correlationKeys [ fn ] = compiled ( nsRow )
396- }
397- return [ key , { ...nsRow , __includesCorrelationKeys : correlationKeys } ]
398- } ) ,
399- )
400- }
401440 }
402441
403442 if ( query . distinct && ! query . fnSelect && ! query . select ) {
@@ -442,6 +481,25 @@ export function compileQuery(
442481 )
443482 }
444483
484+ // Tag $selected with routing metadata for includes.
485+ // This lets collection-config-builder extract routing info (correlationKey + parentContext)
486+ // from parent results without depending on the user's select.
487+ if ( includesRoutingFns . length > 0 ) {
488+ pipeline = pipeline . pipe (
489+ map ( ( [ key , namespacedRow ] : any ) => {
490+ const routing : Record <
491+ string ,
492+ { correlationKey : unknown ; parentContext : Record < string , any > | null }
493+ > = { }
494+ for ( const { fieldName, getRouting } of includesRoutingFns ) {
495+ routing [ fieldName ] = getRouting ( namespacedRow )
496+ }
497+ namespacedRow . $selected [ INCLUDES_ROUTING ] = routing
498+ return [ key , namespacedRow ]
499+ } ) ,
500+ )
501+ }
502+
445503 // Process the GROUP BY clause if it exists
446504 if ( query . groupBy && query . groupBy . length > 0 ) {
447505 pipeline = processGroupBy (
@@ -531,16 +589,14 @@ export function compileQuery(
531589 // Extract the final results from $selected and include orderBy index
532590 const raw = ( row as any ) . $selected
533591 const finalResults = unwrapValue ( raw )
534- // Stamp includes correlation keys onto the result for child routing
535- if ( ( row as any ) . __includesCorrelationKeys ) {
536- finalResults . __includesCorrelationKeys = (
537- row as any
538- ) . __includesCorrelationKeys
539- }
540- // When in includes mode, embed the correlation key as third element
592+ // When in includes mode, embed the correlation key and parentContext
541593 if ( parentKeyStream ) {
542594 const correlationKey = ( row as any ) [ mainSource ] ?. __correlationKey
543- return [ key , [ finalResults , orderByIndex , correlationKey ] ] as any
595+ const parentContext = ( row as any ) . __parentContext ?? null
596+ return [
597+ key ,
598+ [ finalResults , orderByIndex , correlationKey , parentContext ] ,
599+ ] as any
544600 }
545601 return [ key , [ finalResults , orderByIndex ] ] as [ unknown , [ any , string ] ]
546602 } ) ,
@@ -570,16 +626,14 @@ export function compileQuery(
570626 // Extract the final results from $selected and return [key, [results, undefined]]
571627 const raw = ( row as any ) . $selected
572628 const finalResults = unwrapValue ( raw )
573- // Stamp includes correlation keys onto the result for child routing
574- if ( ( row as any ) . __includesCorrelationKeys ) {
575- finalResults . __includesCorrelationKeys = (
576- row as any
577- ) . __includesCorrelationKeys
578- }
579- // When in includes mode, embed the correlation key as third element
629+ // When in includes mode, embed the correlation key and parentContext
580630 if ( parentKeyStream ) {
581631 const correlationKey = ( row as any ) [ mainSource ] ?. __correlationKey
582- return [ key , [ finalResults , undefined , correlationKey ] ] as any
632+ const parentContext = ( row as any ) . __parentContext ?? null
633+ return [
634+ key ,
635+ [ finalResults , undefined , correlationKey , parentContext ] ,
636+ ] as any
583637 }
584638 return [ key , [ finalResults , undefined ] ] as [
585639 unknown ,
0 commit comments