Skip to content

Commit 6e075ee

Browse files
branch-4.0: [fix](search) add restriction for search function #56706 (#56707)
Cherry-picked from #56706 Co-authored-by: Jack <jiangkai@selectdb.com>
1 parent f1fbf47 commit 6e075ee

6 files changed

Lines changed: 625 additions & 0 deletions

File tree

fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Analyzer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.apache.doris.nereids.rules.analysis.CheckAfterBind;
3030
import org.apache.doris.nereids.rules.analysis.CheckAnalysis;
3131
import org.apache.doris.nereids.rules.analysis.CheckPolicy;
32+
import org.apache.doris.nereids.rules.analysis.CheckSearchUsage;
3233
import org.apache.doris.nereids.rules.analysis.CollectJoinConstraint;
3334
import org.apache.doris.nereids.rules.analysis.CollectSubQueryAlias;
3435
import org.apache.doris.nereids.rules.analysis.CompressedMaterialize;
@@ -177,6 +178,8 @@ private static List<RewriteJob> buildAnalyzerJobs() {
177178
// @t_zone must be replaced as 'GMT' before EliminateGroupByConstant and NormalizeAggregate rule.
178179
// So need run VariableToLiteral rule before the two rules.
179180
topDown(new VariableToLiteral()),
181+
// run CheckSearchUsage before CheckAnalysis to detect search() in GROUP BY before it gets optimized
182+
bottomUp(new CheckSearchUsage()),
180183
// run CheckAnalysis before EliminateGroupByConstant in order to report error message correctly like bellow
181184
// select SUM(lo_tax) FROM lineorder group by 1;
182185
// errCode = 2, detailMessage = GROUP BY expression must not contain aggregate functions: sum(lo_tax)

fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ public enum RuleType {
135135
CHECK_AND_STANDARDIZE_WINDOW_FUNCTION_AND_FRAME(RuleTypeClass.REWRITE),
136136
CHECK_MATCH_EXPRESSION(RuleTypeClass.REWRITE),
137137
REWRITE_SEARCH_TO_SLOTS(RuleTypeClass.REWRITE),
138+
CHECK_SEARCH_USAGE(RuleTypeClass.REWRITE),
138139
CREATE_PARTITION_TOPN_FOR_WINDOW(RuleTypeClass.REWRITE),
139140
AGGREGATE_DISASSEMBLE(RuleTypeClass.REWRITE),
140141
SIMPLIFY_AGG_GROUP_BY(RuleTypeClass.REWRITE),
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.doris.nereids.rules.analysis;
19+
20+
import org.apache.doris.nereids.exceptions.AnalysisException;
21+
import org.apache.doris.nereids.rules.Rule;
22+
import org.apache.doris.nereids.rules.RuleType;
23+
import org.apache.doris.nereids.trees.expressions.Expression;
24+
import org.apache.doris.nereids.trees.expressions.SearchExpression;
25+
import org.apache.doris.nereids.trees.expressions.functions.scalar.Search;
26+
import org.apache.doris.nereids.trees.plans.Plan;
27+
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
28+
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
29+
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
30+
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
31+
32+
import com.google.common.collect.ImmutableList;
33+
34+
import java.util.List;
35+
36+
/**
37+
* Check search expression usage - search() can only be used in WHERE filters on single-table OLAP scans.
38+
* This rule validates that search() expressions only appear in supported contexts.
39+
* Must run in analysis phase before search() gets optimized away.
40+
*/
41+
public class CheckSearchUsage implements AnalysisRuleFactory {
42+
43+
@Override
44+
public List<Rule> buildRules() {
45+
return ImmutableList.of(
46+
any().thenApply(ctx -> {
47+
Plan plan = ctx.root;
48+
checkPlanRecursively(plan);
49+
return plan;
50+
}).toRule(RuleType.CHECK_SEARCH_USAGE)
51+
);
52+
}
53+
54+
private void checkPlanRecursively(Plan plan) {
55+
// Check if current plan node contains search expressions
56+
if (containsSearchInPlanExpressions(plan)) {
57+
validateSearchUsage(plan);
58+
}
59+
60+
// Check aggregate nodes specifically for GROUP BY usage
61+
if (plan instanceof LogicalAggregate) {
62+
LogicalAggregate<?> agg = (LogicalAggregate<?>) plan;
63+
for (Expression expr : agg.getGroupByExpressions()) {
64+
if (containsSearchExpression(expr)) {
65+
throw new AnalysisException("search() cannot appear in GROUP BY expressions; "
66+
+ "search predicates are only supported in WHERE filters on single-table scans");
67+
}
68+
}
69+
for (Expression expr : agg.getOutputExpressions()) {
70+
if (containsSearchExpression(expr)) {
71+
throw new AnalysisException("search() cannot appear in aggregate output expressions; "
72+
+ "search predicates are only supported in WHERE filters on single-table scans");
73+
}
74+
}
75+
}
76+
77+
// Check project nodes
78+
if (plan instanceof LogicalProject) {
79+
LogicalProject<?> project = (LogicalProject<?>) plan;
80+
for (Expression expr : project.getProjects()) {
81+
if (containsSearchExpression(expr)) {
82+
// Only allow if it's the project directly above a filter->scan pattern
83+
throw new AnalysisException("search() can only appear in WHERE filters on OLAP scans; "
84+
+ "projection of search() is not supported");
85+
}
86+
}
87+
}
88+
89+
// Recursively check children
90+
for (Plan child : plan.children()) {
91+
checkPlanRecursively(child);
92+
}
93+
}
94+
95+
private void validateSearchUsage(Plan plan) {
96+
if (plan instanceof LogicalFilter) {
97+
Plan child = plan.child(0);
98+
if (!(child instanceof LogicalOlapScan)) {
99+
throw new AnalysisException("search() predicate only supports filtering directly on a single "
100+
+ "table scan; remove joins, subqueries, or additional operators between search() "
101+
+ "and the target table");
102+
}
103+
} else if (!(plan instanceof LogicalProject)) {
104+
// search() can only appear in LogicalFilter or specific LogicalProject nodes
105+
throw new AnalysisException("search() predicates are only supported inside WHERE filters on "
106+
+ "single-table scans");
107+
}
108+
}
109+
110+
private boolean containsSearchInPlanExpressions(Plan plan) {
111+
for (Expression expr : plan.getExpressions()) {
112+
if (containsSearchExpression(expr)) {
113+
return true;
114+
}
115+
}
116+
return false;
117+
}
118+
119+
private boolean containsSearchExpression(Expression expression) {
120+
if (expression instanceof Search || expression instanceof SearchExpression) {
121+
return true;
122+
}
123+
for (Expression child : expression.children()) {
124+
if (containsSearchExpression(child)) {
125+
return true;
126+
}
127+
}
128+
return false;
129+
}
130+
}

0 commit comments

Comments
 (0)