@@ -686,21 +686,65 @@ impl FilterTagging {
686686 }
687687
688688 // Get the label for this table
689+ // First try plan_ctx (works for typed patterns)
690+ // If None, try to find it in the plan tree (works for UNION branches where
691+ // each branch has a typed GraphNode but the global plan_ctx still has untyped entry)
689692 let label = match table_ctx. get_label_opt ( ) {
690- Some ( l) => l,
691- None => {
692- // No label yet - this is an untyped pattern like MATCH (n)
693- // Property validation will happen during scan generation when we filter types
694- // For now, pass through the property access as-is
695- log:: info!(
696- "🔧 FilterTagging: Skipping validation for untyped pattern '{}', property='{}' (will validate during scan generation)" ,
697- property_access. table_alias. 0 ,
698- property_access. column. raw( )
699- ) ;
700- return Ok ( LogicalExpr :: PropertyAccessExp ( property_access) ) ;
693+ Some ( l) if !l. is_empty ( ) => l, // Skip empty labels (from pruned branches)
694+ _ => {
695+ // Try to get label from the plan tree (for UNION branches)
696+ if let Some ( plan_ref) = plan {
697+ if let Some ( label_from_plan) =
698+ Self :: find_label_in_plan ( plan_ref, & property_access. table_alias . 0 )
699+ {
700+ if label_from_plan. is_empty ( ) {
701+ // Empty label means this branch was pruned (no matching types)
702+ // Skip property mapping - the branch will return 0 rows anyway
703+ log:: info!(
704+ "🔧 FilterTagging: Skipping property mapping for pruned branch '{}', property='{}'" ,
705+ property_access. table_alias. 0 ,
706+ property_access. column. raw( )
707+ ) ;
708+ return Ok ( LogicalExpr :: PropertyAccessExp ( property_access) ) ;
709+ }
710+ log:: info!(
711+ "🔧 FilterTagging: Found label '{}' from plan tree for untyped pattern '{}', property='{}'" ,
712+ label_from_plan,
713+ property_access. table_alias. 0 ,
714+ property_access. column. raw( )
715+ ) ;
716+ label_from_plan
717+ } else {
718+ // No label in plan either - truly untyped, pass through as-is
719+ log:: info!(
720+ "🔧 FilterTagging: Skipping validation for untyped pattern '{}', property='{}' (will validate during scan generation)" ,
721+ property_access. table_alias. 0 ,
722+ property_access. column. raw( )
723+ ) ;
724+ return Ok ( LogicalExpr :: PropertyAccessExp ( property_access) ) ;
725+ }
726+ } else {
727+ // No plan reference - pass through as-is
728+ log:: info!(
729+ "🔧 FilterTagging: Skipping validation for untyped pattern '{}', property='{}' (no plan context)" ,
730+ property_access. table_alias. 0 ,
731+ property_access. column. raw( )
732+ ) ;
733+ return Ok ( LogicalExpr :: PropertyAccessExp ( property_access) ) ;
734+ }
701735 }
702736 } ;
703737
738+ // Handle empty label case (branch was pruned by property-based filtering)
739+ if label. is_empty ( ) {
740+ log:: info!(
741+ "🔧 FilterTagging: Skipping property mapping for pruned branch '{}' (empty label), property='{}'" ,
742+ property_access. table_alias. 0 ,
743+ property_access. column. raw( )
744+ ) ;
745+ return Ok ( LogicalExpr :: PropertyAccessExp ( property_access) ) ;
746+ }
747+
704748 // Check if this node uses EmbeddedInEdge strategy (denormalized access)
705749 let ( is_embedded_in_edge, _owning_edge_info) = if let Some ( plan) = plan {
706750 // Also check by traversing the plan to find which edge owns this node
@@ -1680,6 +1724,44 @@ impl FilterTagging {
16801724 }
16811725 }
16821726
1727+ /// Find the label for an alias by looking at GraphNode in the plan tree
1728+ /// This is used for UNION branches where the branch has a typed GraphNode
1729+ /// but the global plan_ctx still has the untyped entry
1730+ fn find_label_in_plan ( plan : & LogicalPlan , alias : & str ) -> Option < String > {
1731+ match plan {
1732+ LogicalPlan :: GraphNode ( node) => {
1733+ if node. alias == alias {
1734+ return node. label . clone ( ) ;
1735+ }
1736+ Self :: find_label_in_plan ( & node. input , alias)
1737+ }
1738+ LogicalPlan :: Filter ( filter) => Self :: find_label_in_plan ( & filter. input , alias) ,
1739+ LogicalPlan :: Projection ( proj) => Self :: find_label_in_plan ( & proj. input , alias) ,
1740+ LogicalPlan :: GraphJoins ( joins) => Self :: find_label_in_plan ( & joins. input , alias) ,
1741+ LogicalPlan :: GroupBy ( gb) => Self :: find_label_in_plan ( & gb. input , alias) ,
1742+ LogicalPlan :: OrderBy ( ob) => Self :: find_label_in_plan ( & ob. input , alias) ,
1743+ LogicalPlan :: Skip ( skip) => Self :: find_label_in_plan ( & skip. input , alias) ,
1744+ LogicalPlan :: Limit ( limit) => Self :: find_label_in_plan ( & limit. input , alias) ,
1745+ LogicalPlan :: Cte ( cte) => Self :: find_label_in_plan ( & cte. input , alias) ,
1746+ LogicalPlan :: GraphRel ( rel) => {
1747+ if let Some ( l) = Self :: find_label_in_plan ( & rel. left , alias) {
1748+ return Some ( l) ;
1749+ }
1750+ if let Some ( l) = Self :: find_label_in_plan ( & rel. center , alias) {
1751+ return Some ( l) ;
1752+ }
1753+ Self :: find_label_in_plan ( & rel. right , alias)
1754+ }
1755+ LogicalPlan :: CartesianProduct ( cp) => {
1756+ if let Some ( l) = Self :: find_label_in_plan ( & cp. left , alias) {
1757+ return Some ( l) ;
1758+ }
1759+ Self :: find_label_in_plan ( & cp. right , alias)
1760+ }
1761+ _ => None ,
1762+ }
1763+ }
1764+
16831765 /// Check if a plan has a CartesianProduct descendant
16841766 /// Used to identify cross-table join conditions that should not be extracted
16851767 fn has_cartesian_product_descendant ( plan : & LogicalPlan ) -> bool {
0 commit comments