@@ -176,31 +176,13 @@ public CalciteRelNodeVisitor(DataSourceService dataSourceService) {
176176 }
177177
178178 public RelNode analyze (UnresolvedPlan unresolved , CalcitePlanContext context ) {
179- // Enable filter accumulation if this plan contains multiple filtering operations
180- // that could create deep Filter RelNode chains
181- if (countFilteringOperations (unresolved ) >= 2 ) {
182- context .enableFilterAccumulation ();
183- try {
184- unresolved .accept (this , context );
185- context .flushFilterConditions ();
186- return context .relBuilder .peek ();
187- } finally {
188- context .disableFilterAccumulation ();
189- }
190- } else {
191- return unresolved .accept (this , context );
192- }
193- }
179+ // Build the RelNode tree (may contain deep Filter chains)
180+ RelNode relNode = unresolved .accept (this , context );
194181
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- }
182+ // Apply filter merge optimization as post-processing
183+ // This merges consecutive LogicalFilter nodes to prevent OOM with deep chains
184+ FilterMergeVisitor filterMergeVisitor = new FilterMergeVisitor ();
185+ return relNode .accept (filterMergeVisitor );
204186 }
205187
206188 @ Override
@@ -268,12 +250,7 @@ public RelNode visitFilter(Filter node, CalcitePlanContext context) {
268250 context .relBuilder .filter (ImmutableList .of (v .get ().id ), condition );
269251 context .popCorrelVar ();
270252 } else {
271- // Use filter accumulation to prevent deep Filter node chains
272- if (context .isFilterAccumulationEnabled ()) {
273- context .addFilterCondition (condition );
274- } else {
275- context .relBuilder .filter (condition );
276- }
253+ context .relBuilder .filter (condition );
277254 }
278255 return context .relBuilder .peek ();
279256 }
@@ -322,20 +299,13 @@ public RelNode visitRegex(Regex node, CalcitePlanContext context) {
322299 regexCondition = context .rexBuilder .makeCall (SqlStdOperatorTable .NOT , regexCondition );
323300 }
324301
325- // Use filter accumulation to prevent deep Filter node chains
326- if (context .isFilterAccumulationEnabled ()) {
327- context .addFilterCondition (regexCondition );
328- } else {
329- context .relBuilder .filter (regexCondition );
330- }
302+ context .relBuilder .filter (regexCondition );
331303 return context .relBuilder .peek ();
332304 }
333305
334306 public RelNode visitRex (Rex node , CalcitePlanContext context ) {
335307 visitChildren (node , context );
336308
337- flushFiltersBeforeSchemaChange (context );
338-
339309 RexNode fieldRex = rexVisitor .analyze (node .getField (), context );
340310 String patternStr = (String ) node .getPattern ().getValue ();
341311
@@ -420,8 +390,6 @@ private boolean containsSubqueryExpression(Node expr) {
420390 public RelNode visitProject (Project node , CalcitePlanContext context ) {
421391 visitChildren (node , context );
422392
423- flushFiltersBeforeSchemaChange (context );
424-
425393 if (isSingleAllFieldsProject (node )) {
426394 return handleAllFieldsProject (node , context );
427395 }
@@ -736,8 +704,6 @@ public RelNode visitReverse(
736704 public RelNode visitBin (Bin node , CalcitePlanContext context ) {
737705 visitChildren (node , context );
738706
739- flushFiltersBeforeSchemaChange (context );
740-
741707 RexNode fieldExpr = rexVisitor .analyze (node .getField (), context );
742708 String fieldName = BinUtils .extractFieldName (node );
743709
@@ -752,7 +718,6 @@ public RelNode visitBin(Bin node, CalcitePlanContext context) {
752718 @ Override
753719 public RelNode visitParse (Parse node , CalcitePlanContext context ) {
754720 visitChildren (node , context );
755- flushFiltersBeforeSchemaChange (context );
756721 buildParseRelNode (node , context );
757722 return context .relBuilder .peek ();
758723 }
@@ -900,8 +865,6 @@ public RelNode visitPatterns(Patterns node, CalcitePlanContext context) {
900865 public RelNode visitEval (Eval node , CalcitePlanContext context ) {
901866 visitChildren (node , context );
902867
903- flushFiltersBeforeSchemaChange (context );
904-
905868 node .getExpressionList ()
906869 .forEach (
907870 expr -> {
@@ -1171,9 +1134,6 @@ private Pair<List<RexNode>, List<AggCall>> resolveAttributesForAggregation(
11711134 /** Visits an aggregation for stats command */
11721135 @ Override
11731136 public RelNode visitAggregation (Aggregation node , CalcitePlanContext context ) {
1174- // Flush accumulated filter conditions before schema-changing aggregation operations
1175- flushFiltersBeforeSchemaChange (context );
1176-
11771137 Argument .ArgumentMap statsArgs = Argument .ArgumentMap .of (node .getArgExprList ());
11781138 Boolean bucketNullable = (Boolean ) statsArgs .get (Argument .BUCKET_NULLABLE ).getValue ();
11791139 int nGroup = node .getGroupExprList ().size () + (Objects .nonNull (node .getSpan ()) ? 1 : 0 );
@@ -2292,26 +2252,11 @@ private RelNode mergeTableAndResolveColumnConflict(
22922252 @ Override
22932253 public RelNode visitMultisearch (Multisearch node , CalcitePlanContext context ) {
22942254 List <RelNode > subsearchNodes = new ArrayList <>();
2295- // Save the current filter accumulation state - we'll process each subsearch independently
2296- boolean wasFilterAccumulationEnabled = context .isFilterAccumulationEnabled ();
22972255
22982256 for (UnresolvedPlan subsearch : node .getSubsearches ()) {
22992257 UnresolvedPlan prunedSubSearch = subsearch .accept (new EmptySourcePropagateVisitor (), null );
2300-
2301- // Temporarily disable filter accumulation so each subsearch gets its own independent
2302- // lifecycle via analyze(). This prevents filter state from bleeding across branches.
2303- if (wasFilterAccumulationEnabled ) {
2304- context .disableFilterAccumulation ();
2305- }
2306-
2307- // Use analyze() to let each subsearch determine its own filter accumulation needs
23082258 analyze (prunedSubSearch , context );
23092259 subsearchNodes .add (context .relBuilder .build ());
2310-
2311- // Restore filter accumulation state for the next iteration
2312- if (wasFilterAccumulationEnabled ) {
2313- context .enableFilterAccumulation ();
2314- }
23152260 }
23162261
23172262 // Use shared schema merging logic that handles type conflicts via field renaming
@@ -3302,82 +3247,4 @@ private RexNode createOptimizedTransliteration(
33023247 throw new RuntimeException ("Failed to optimize sed expression: " + sedExpression , e );
33033248 }
33043249 }
3305-
3306- /**
3307- * Counts the number of filtering operations in an UnresolvedPlan tree that would create Filter
3308- * RelNodes. This is used to detect queries with multiple regex/filter operations that could cause
3309- * deep Filter RelNode chains and memory exhaustion.
3310- *
3311- * <p>Stops counting at schema-changing operations (like Aggregation, Project with computed
3312- * expressions) to avoid enabling filter accumulation across schema boundaries, which would cause
3313- * RexInputRef index mismatches.
3314- *
3315- * @param plan the UnresolvedPlan to analyze
3316- * @return the count of filtering operations found before the first schema-changing operation
3317- */
3318- private int countFilteringOperations (UnresolvedPlan plan ) {
3319- if (plan == null ) {
3320- return 0 ;
3321- }
3322-
3323- int count = 0 ;
3324-
3325- // Count this node if it's a filtering operation
3326- // BUT: Don't count Filter nodes that contain function calls, as they can cause
3327- // type mismatches when accumulated and flushed later
3328- if (plan instanceof Regex ) {
3329- count = 1 ;
3330- } else if (plan instanceof Filter ) {
3331- Filter filterNode = (Filter ) plan ;
3332- if (!containsFunctionCall (filterNode .getCondition ())) {
3333- count = 1 ;
3334- }
3335- }
3336-
3337- // Stop counting at schema-changing operations to prevent accumulation across schema boundaries
3338- // Schema-changing operations include: Aggregation, Eval, Project (with computed expressions),
3339- // Window, StreamWindow, etc.
3340- if (plan instanceof Aggregation
3341- || plan instanceof Eval
3342- || plan instanceof Window
3343- || plan instanceof StreamWindow ) {
3344- return count ; // Don't recurse into children beyond schema changes
3345- }
3346-
3347- // Recursively count filtering operations in children
3348- if (plan .getChild () != null ) {
3349- for (Node child : plan .getChild ()) {
3350- if (child instanceof UnresolvedPlan ) {
3351- count += countFilteringOperations ((UnresolvedPlan ) child );
3352- }
3353- }
3354- }
3355-
3356- return count ;
3357- }
3358-
3359- /**
3360- * Checks if an expression contains any function calls. Filter expressions with function calls can
3361- * cause type mismatches when accumulated and flushed later, so we exclude them from filter
3362- * accumulation.
3363- */
3364- private boolean containsFunctionCall (UnresolvedExpression expr ) {
3365- if (expr == null ) {
3366- return false ;
3367- }
3368-
3369- if (expr instanceof org .opensearch .sql .ast .expression .Function ) {
3370- return true ;
3371- }
3372-
3373- // Check children recursively
3374- for (Node child : expr .getChild ()) {
3375- if (child instanceof UnresolvedExpression
3376- && containsFunctionCall ((UnresolvedExpression ) child )) {
3377- return true ;
3378- }
3379- }
3380-
3381- return false ;
3382- }
33833250}
0 commit comments