@@ -182,8 +182,8 @@ public RelNode analyze(UnresolvedPlan unresolved, CalcitePlanContext context) {
182182 context .enableFilterAccumulation ();
183183 try {
184184 unresolved .accept (this , context );
185- context .flushFilterConditions (); // Flush accumulated conditions before returning
186- return context .relBuilder .peek (); // Get the result after flushing
185+ context .flushFilterConditions ();
186+ return context .relBuilder .peek ();
187187 } finally {
188188 context .disableFilterAccumulation ();
189189 }
@@ -192,6 +192,17 @@ public RelNode analyze(UnresolvedPlan unresolved, CalcitePlanContext context) {
192192 }
193193 }
194194
195+ /**
196+ * Flushes accumulated filter conditions before schema-changing operations. This prevents
197+ * RexInputRef index mismatches that occur when filters reference field indices from the old
198+ * schema.
199+ */
200+ private void flushFiltersBeforeSchemaChange (CalcitePlanContext context ) {
201+ if (context .isFilterAccumulationEnabled () && context .hasPendingFilterConditions ()) {
202+ context .flushFilterConditions ();
203+ }
204+ }
205+
195206 @ Override
196207 public RelNode visitRelation (Relation node , CalcitePlanContext context ) {
197208 DataSourceSchemaIdentifierNameResolver nameResolver =
@@ -407,10 +418,7 @@ private boolean containsSubqueryExpression(Node expr) {
407418 public RelNode visitProject (Project node , CalcitePlanContext context ) {
408419 visitChildren (node , context );
409420
410- // Flush accumulated filter conditions before schema-changing operations
411- if (context .isFilterAccumulationEnabled () && context .hasPendingFilterConditions ()) {
412- context .flushFilterConditions ();
413- }
421+ flushFiltersBeforeSchemaChange (context );
414422
415423 if (isSingleAllFieldsProject (node )) {
416424 return handleAllFieldsProject (node , context );
@@ -886,6 +894,9 @@ public RelNode visitPatterns(Patterns node, CalcitePlanContext context) {
886894 @ Override
887895 public RelNode visitEval (Eval node , CalcitePlanContext context ) {
888896 visitChildren (node , context );
897+
898+ flushFiltersBeforeSchemaChange (context );
899+
889900 node .getExpressionList ()
890901 .forEach (
891902 expr -> {
@@ -1155,6 +1166,9 @@ private Pair<List<RexNode>, List<AggCall>> resolveAttributesForAggregation(
11551166 /** Visits an aggregation for stats command */
11561167 @ Override
11571168 public RelNode visitAggregation (Aggregation node , CalcitePlanContext context ) {
1169+ // Flush accumulated filter conditions before schema-changing aggregation operations
1170+ flushFiltersBeforeSchemaChange (context );
1171+
11581172 Argument .ArgumentMap statsArgs = Argument .ArgumentMap .of (node .getArgExprList ());
11591173 Boolean bucketNullable = (Boolean ) statsArgs .get (Argument .BUCKET_NULLABLE ).getValue ();
11601174 int nGroup = node .getGroupExprList ().size () + (Objects .nonNull (node .getSpan ()) ? 1 : 0 );
@@ -2273,10 +2287,26 @@ private RelNode mergeTableAndResolveColumnConflict(
22732287 @ Override
22742288 public RelNode visitMultisearch (Multisearch node , CalcitePlanContext context ) {
22752289 List <RelNode > subsearchNodes = new ArrayList <>();
2290+ // Save the current filter accumulation state - we'll process each subsearch independently
2291+ boolean wasFilterAccumulationEnabled = context .isFilterAccumulationEnabled ();
2292+
22762293 for (UnresolvedPlan subsearch : node .getSubsearches ()) {
22772294 UnresolvedPlan prunedSubSearch = subsearch .accept (new EmptySourcePropagateVisitor (), null );
2278- prunedSubSearch .accept (this , context );
2295+
2296+ // Temporarily disable filter accumulation so each subsearch gets its own independent
2297+ // lifecycle via analyze(). This prevents filter state from bleeding across branches.
2298+ if (wasFilterAccumulationEnabled ) {
2299+ context .disableFilterAccumulation ();
2300+ }
2301+
2302+ // Use analyze() to let each subsearch determine its own filter accumulation needs
2303+ analyze (prunedSubSearch , context );
22792304 subsearchNodes .add (context .relBuilder .build ());
2305+
2306+ // Restore filter accumulation state for the next iteration
2307+ if (wasFilterAccumulationEnabled ) {
2308+ context .enableFilterAccumulation ();
2309+ }
22802310 }
22812311
22822312 // Use shared schema merging logic that handles type conflicts via field renaming
@@ -3273,8 +3303,12 @@ private RexNode createOptimizedTransliteration(
32733303 * RelNodes. This is used to detect queries with multiple regex/filter operations that could cause
32743304 * deep Filter RelNode chains and memory exhaustion.
32753305 *
3306+ * <p>Stops counting at schema-changing operations (like Aggregation, Project with computed
3307+ * expressions) to avoid enabling filter accumulation across schema boundaries, which would cause
3308+ * RexInputRef index mismatches.
3309+ *
32763310 * @param plan the UnresolvedPlan to analyze
3277- * @return the count of filtering operations found
3311+ * @return the count of filtering operations found before the first schema-changing operation
32783312 */
32793313 private int countFilteringOperations (UnresolvedPlan plan ) {
32803314 if (plan == null ) {
@@ -3284,8 +3318,25 @@ private int countFilteringOperations(UnresolvedPlan plan) {
32843318 int count = 0 ;
32853319
32863320 // Count this node if it's a filtering operation
3287- if (plan instanceof Regex || plan instanceof Filter ) {
3321+ // BUT: Don't count Filter nodes that contain function calls, as they can cause
3322+ // type mismatches when accumulated and flushed later
3323+ if (plan instanceof Regex ) {
32883324 count = 1 ;
3325+ } else if (plan instanceof Filter ) {
3326+ Filter filterNode = (Filter ) plan ;
3327+ if (!containsFunctionCall (filterNode .getCondition ())) {
3328+ count = 1 ;
3329+ }
3330+ }
3331+
3332+ // Stop counting at schema-changing operations to prevent accumulation across schema boundaries
3333+ // Schema-changing operations include: Aggregation, Eval, Project (with computed expressions),
3334+ // Window, StreamWindow, etc.
3335+ if (plan instanceof Aggregation
3336+ || plan instanceof Eval
3337+ || plan instanceof Window
3338+ || plan instanceof StreamWindow ) {
3339+ return count ; // Don't recurse into children beyond schema changes
32893340 }
32903341
32913342 // Recursively count filtering operations in children
@@ -3299,4 +3350,29 @@ private int countFilteringOperations(UnresolvedPlan plan) {
32993350
33003351 return count ;
33013352 }
3353+
3354+ /**
3355+ * Checks if an expression contains any function calls. Filter expressions with function calls can
3356+ * cause type mismatches when accumulated and flushed later, so we exclude them from filter
3357+ * accumulation.
3358+ */
3359+ private boolean containsFunctionCall (UnresolvedExpression expr ) {
3360+ if (expr == null ) {
3361+ return false ;
3362+ }
3363+
3364+ if (expr instanceof org .opensearch .sql .ast .expression .Function ) {
3365+ return true ;
3366+ }
3367+
3368+ // Check children recursively
3369+ for (Node child : expr .getChild ()) {
3370+ if (child instanceof UnresolvedExpression
3371+ && containsFunctionCall ((UnresolvedExpression ) child )) {
3372+ return true ;
3373+ }
3374+ }
3375+
3376+ return false ;
3377+ }
33023378}
0 commit comments