@@ -183,8 +183,8 @@ public RelNode analyze(UnresolvedPlan unresolved, CalcitePlanContext context) {
183183 context .enableFilterAccumulation ();
184184 try {
185185 unresolved .accept (this , context );
186- context .flushFilterConditions (); // Flush accumulated conditions before returning
187- return context .relBuilder .peek (); // Get the result after flushing
186+ context .flushFilterConditions ();
187+ return context .relBuilder .peek ();
188188 } finally {
189189 context .disableFilterAccumulation ();
190190 }
@@ -193,6 +193,17 @@ public RelNode analyze(UnresolvedPlan unresolved, CalcitePlanContext context) {
193193 }
194194 }
195195
196+ /**
197+ * Flushes accumulated filter conditions before schema-changing operations. This prevents
198+ * RexInputRef index mismatches that occur when filters reference field indices from the old
199+ * schema.
200+ */
201+ private void flushFiltersBeforeSchemaChange (CalcitePlanContext context ) {
202+ if (context .isFilterAccumulationEnabled () && context .hasPendingFilterConditions ()) {
203+ context .flushFilterConditions ();
204+ }
205+ }
206+
196207 @ Override
197208 public RelNode visitRelation (Relation node , CalcitePlanContext context ) {
198209 DataSourceSchemaIdentifierNameResolver nameResolver =
@@ -404,10 +415,7 @@ private boolean containsSubqueryExpression(Node expr) {
404415 public RelNode visitProject (Project node , CalcitePlanContext context ) {
405416 visitChildren (node , context );
406417
407- // Flush accumulated filter conditions before schema-changing operations
408- if (context .isFilterAccumulationEnabled () && context .hasPendingFilterConditions ()) {
409- context .flushFilterConditions ();
410- }
418+ flushFiltersBeforeSchemaChange (context );
411419
412420 if (isSingleAllFieldsProject (node )) {
413421 return handleAllFieldsProject (node , context );
@@ -883,6 +891,9 @@ public RelNode visitPatterns(Patterns node, CalcitePlanContext context) {
883891 @ Override
884892 public RelNode visitEval (Eval node , CalcitePlanContext context ) {
885893 visitChildren (node , context );
894+
895+ flushFiltersBeforeSchemaChange (context );
896+
886897 node .getExpressionList ()
887898 .forEach (
888899 expr -> {
@@ -1152,6 +1163,9 @@ private Pair<List<RexNode>, List<AggCall>> resolveAttributesForAggregation(
11521163 /** Visits an aggregation for stats command */
11531164 @ Override
11541165 public RelNode visitAggregation (Aggregation node , CalcitePlanContext context ) {
1166+ // Flush accumulated filter conditions before schema-changing aggregation operations
1167+ flushFiltersBeforeSchemaChange (context );
1168+
11551169 Argument .ArgumentMap statsArgs = Argument .ArgumentMap .of (node .getArgExprList ());
11561170 Boolean bucketNullable =
11571171 (Boolean ) statsArgs .getOrDefault (Argument .BUCKET_NULLABLE , Literal .TRUE ).getValue ();
@@ -2252,10 +2266,26 @@ private RelNode mergeTableAndResolveColumnConflict(
22522266 @ Override
22532267 public RelNode visitMultisearch (Multisearch node , CalcitePlanContext context ) {
22542268 List <RelNode > subsearchNodes = new ArrayList <>();
2269+ // Save the current filter accumulation state - we'll process each subsearch independently
2270+ boolean wasFilterAccumulationEnabled = context .isFilterAccumulationEnabled ();
2271+
22552272 for (UnresolvedPlan subsearch : node .getSubsearches ()) {
22562273 UnresolvedPlan prunedSubSearch = subsearch .accept (new EmptySourcePropagateVisitor (), null );
2257- prunedSubSearch .accept (this , context );
2274+
2275+ // Temporarily disable filter accumulation so each subsearch gets its own independent
2276+ // lifecycle via analyze(). This prevents filter state from bleeding across branches.
2277+ if (wasFilterAccumulationEnabled ) {
2278+ context .disableFilterAccumulation ();
2279+ }
2280+
2281+ // Use analyze() to let each subsearch determine its own filter accumulation needs
2282+ analyze (prunedSubSearch , context );
22582283 subsearchNodes .add (context .relBuilder .build ());
2284+
2285+ // Restore filter accumulation state for the next iteration
2286+ if (wasFilterAccumulationEnabled ) {
2287+ context .enableFilterAccumulation ();
2288+ }
22592289 }
22602290
22612291 // Use shared schema merging logic that handles type conflicts via field renaming
@@ -3271,8 +3301,12 @@ private RexNode createOptimizedTransliteration(
32713301 * RelNodes. This is used to detect queries with multiple regex/filter operations that could cause
32723302 * deep Filter RelNode chains and memory exhaustion.
32733303 *
3304+ * <p>Stops counting at schema-changing operations (like Aggregation, Project with computed
3305+ * expressions) to avoid enabling filter accumulation across schema boundaries, which would cause
3306+ * RexInputRef index mismatches.
3307+ *
32743308 * @param plan the UnresolvedPlan to analyze
3275- * @return the count of filtering operations found
3309+ * @return the count of filtering operations found before the first schema-changing operation
32763310 */
32773311 private int countFilteringOperations (UnresolvedPlan plan ) {
32783312 if (plan == null ) {
@@ -3282,8 +3316,25 @@ private int countFilteringOperations(UnresolvedPlan plan) {
32823316 int count = 0 ;
32833317
32843318 // Count this node if it's a filtering operation
3285- if (plan instanceof Regex || plan instanceof Filter ) {
3319+ // BUT: Don't count Filter nodes that contain function calls, as they can cause
3320+ // type mismatches when accumulated and flushed later
3321+ if (plan instanceof Regex ) {
32863322 count = 1 ;
3323+ } else if (plan instanceof Filter ) {
3324+ Filter filterNode = (Filter ) plan ;
3325+ if (!containsFunctionCall (filterNode .getCondition ())) {
3326+ count = 1 ;
3327+ }
3328+ }
3329+
3330+ // Stop counting at schema-changing operations to prevent accumulation across schema boundaries
3331+ // Schema-changing operations include: Aggregation, Eval, Project (with computed expressions),
3332+ // Window, StreamWindow, etc.
3333+ if (plan instanceof Aggregation
3334+ || plan instanceof Eval
3335+ || plan instanceof Window
3336+ || plan instanceof StreamWindow ) {
3337+ return count ; // Don't recurse into children beyond schema changes
32873338 }
32883339
32893340 // Recursively count filtering operations in children
@@ -3297,4 +3348,29 @@ private int countFilteringOperations(UnresolvedPlan plan) {
32973348
32983349 return count ;
32993350 }
3351+
3352+ /**
3353+ * Checks if an expression contains any function calls. Filter expressions with function calls can
3354+ * cause type mismatches when accumulated and flushed later, so we exclude them from filter
3355+ * accumulation.
3356+ */
3357+ private boolean containsFunctionCall (UnresolvedExpression expr ) {
3358+ if (expr == null ) {
3359+ return false ;
3360+ }
3361+
3362+ if (expr instanceof org .opensearch .sql .ast .expression .Function ) {
3363+ return true ;
3364+ }
3365+
3366+ // Check children recursively
3367+ for (Node child : expr .getChild ()) {
3368+ if (child instanceof UnresolvedExpression
3369+ && containsFunctionCall ((UnresolvedExpression ) child )) {
3370+ return true ;
3371+ }
3372+ }
3373+
3374+ return false ;
3375+ }
33003376}
0 commit comments