1 | /* | |
2 | * Copyright OpenSearch Contributors | |
3 | * SPDX-License-Identifier: Apache-2.0 | |
4 | */ | |
5 | ||
6 | ||
7 | package org.opensearch.sql.analysis; | |
8 | ||
9 | import com.google.common.collect.ImmutableList; | |
10 | import com.google.common.collect.ImmutableMap; | |
11 | import com.google.common.collect.ImmutableSet; | |
12 | import java.util.ArrayList; | |
13 | import java.util.Arrays; | |
14 | import java.util.Collections; | |
15 | import java.util.List; | |
16 | import java.util.Optional; | |
17 | import java.util.stream.Collectors; | |
18 | import lombok.Getter; | |
19 | import org.opensearch.sql.analysis.symbol.Namespace; | |
20 | import org.opensearch.sql.analysis.symbol.Symbol; | |
21 | import org.opensearch.sql.ast.AbstractNodeVisitor; | |
22 | import org.opensearch.sql.ast.expression.AggregateFunction; | |
23 | import org.opensearch.sql.ast.expression.AllFields; | |
24 | import org.opensearch.sql.ast.expression.And; | |
25 | import org.opensearch.sql.ast.expression.Argument; | |
26 | import org.opensearch.sql.ast.expression.Case; | |
27 | import org.opensearch.sql.ast.expression.Cast; | |
28 | import org.opensearch.sql.ast.expression.Compare; | |
29 | import org.opensearch.sql.ast.expression.ConstantFunction; | |
30 | import org.opensearch.sql.ast.expression.EqualTo; | |
31 | import org.opensearch.sql.ast.expression.Field; | |
32 | import org.opensearch.sql.ast.expression.Function; | |
33 | import org.opensearch.sql.ast.expression.HighlightFunction; | |
34 | import org.opensearch.sql.ast.expression.In; | |
35 | import org.opensearch.sql.ast.expression.Interval; | |
36 | import org.opensearch.sql.ast.expression.Literal; | |
37 | import org.opensearch.sql.ast.expression.Not; | |
38 | import org.opensearch.sql.ast.expression.Or; | |
39 | import org.opensearch.sql.ast.expression.QualifiedName; | |
40 | import org.opensearch.sql.ast.expression.RelevanceFieldList; | |
41 | import org.opensearch.sql.ast.expression.Span; | |
42 | import org.opensearch.sql.ast.expression.UnresolvedArgument; | |
43 | import org.opensearch.sql.ast.expression.UnresolvedAttribute; | |
44 | import org.opensearch.sql.ast.expression.UnresolvedExpression; | |
45 | import org.opensearch.sql.ast.expression.When; | |
46 | import org.opensearch.sql.ast.expression.WindowFunction; | |
47 | import org.opensearch.sql.ast.expression.Xor; | |
48 | import org.opensearch.sql.common.antlr.SyntaxCheckException; | |
49 | import org.opensearch.sql.data.model.ExprValueUtils; | |
50 | import org.opensearch.sql.data.type.ExprType; | |
51 | import org.opensearch.sql.exception.SemanticCheckException; | |
52 | import org.opensearch.sql.expression.DSL; | |
53 | import org.opensearch.sql.expression.Expression; | |
54 | import org.opensearch.sql.expression.HighlightExpression; | |
55 | import org.opensearch.sql.expression.LiteralExpression; | |
56 | import org.opensearch.sql.expression.NamedArgumentExpression; | |
57 | import org.opensearch.sql.expression.NamedExpression; | |
58 | import org.opensearch.sql.expression.ReferenceExpression; | |
59 | import org.opensearch.sql.expression.aggregation.AggregationState; | |
60 | import org.opensearch.sql.expression.aggregation.Aggregator; | |
61 | import org.opensearch.sql.expression.conditional.cases.CaseClause; | |
62 | import org.opensearch.sql.expression.conditional.cases.WhenClause; | |
63 | import org.opensearch.sql.expression.function.BuiltinFunctionName; | |
64 | import org.opensearch.sql.expression.function.BuiltinFunctionRepository; | |
65 | import org.opensearch.sql.expression.function.FunctionName; | |
66 | import org.opensearch.sql.expression.parse.ParseExpression; | |
67 | import org.opensearch.sql.expression.span.SpanExpression; | |
68 | import org.opensearch.sql.expression.window.aggregation.AggregateWindowFunction; | |
69 | ||
70 | /** | |
71 | * Analyze the {@link UnresolvedExpression} in the {@link AnalysisContext} to construct the {@link | |
72 | * Expression}. | |
73 | */ | |
74 | public class ExpressionAnalyzer extends AbstractNodeVisitor<Expression, AnalysisContext> { | |
75 | @Getter | |
76 | private final BuiltinFunctionRepository repository; | |
77 | private final DSL dsl; | |
78 | ||
79 | @Override | |
80 | public Expression visitCast(Cast node, AnalysisContext context) { | |
81 | final Expression expression = node.getExpression().accept(this, context); | |
82 |
1
1. visitCast : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitCast → KILLED |
return (Expression) repository |
83 | .compile(node.convertFunctionName(), Collections.singletonList(expression)); | |
84 | } | |
85 | ||
86 | public ExpressionAnalyzer( | |
87 | BuiltinFunctionRepository repository) { | |
88 | this.repository = repository; | |
89 | this.dsl = new DSL(repository); | |
90 | } | |
91 | ||
92 | public Expression analyze(UnresolvedExpression unresolved, AnalysisContext context) { | |
93 |
1
1. analyze : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::analyze → KILLED |
return unresolved.accept(this, context); |
94 | } | |
95 | ||
96 | @Override | |
97 | public Expression visitUnresolvedAttribute(UnresolvedAttribute node, AnalysisContext context) { | |
98 |
1
1. visitUnresolvedAttribute : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitUnresolvedAttribute → KILLED |
return visitIdentifier(node.getAttr(), context); |
99 | } | |
100 | ||
101 | @Override | |
102 | public Expression visitEqualTo(EqualTo node, AnalysisContext context) { | |
103 | Expression left = node.getLeft().accept(this, context); | |
104 | Expression right = node.getRight().accept(this, context); | |
105 | ||
106 |
1
1. visitEqualTo : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitEqualTo → KILLED |
return dsl.equal(left, right); |
107 | } | |
108 | ||
109 | @Override | |
110 | public Expression visitLiteral(Literal node, AnalysisContext context) { | |
111 |
1
1. visitLiteral : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitLiteral → KILLED |
return DSL |
112 | .literal(ExprValueUtils.fromObjectValue(node.getValue(), node.getType().getCoreType())); | |
113 | } | |
114 | ||
115 | @Override | |
116 | public Expression visitInterval(Interval node, AnalysisContext context) { | |
117 | Expression value = node.getValue().accept(this, context); | |
118 | Expression unit = DSL.literal(node.getUnit().name()); | |
119 |
1
1. visitInterval : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitInterval → KILLED |
return dsl.interval(value, unit); |
120 | } | |
121 | ||
122 | @Override | |
123 | public Expression visitAnd(And node, AnalysisContext context) { | |
124 | Expression left = node.getLeft().accept(this, context); | |
125 | Expression right = node.getRight().accept(this, context); | |
126 | ||
127 |
1
1. visitAnd : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitAnd → KILLED |
return dsl.and(left, right); |
128 | } | |
129 | ||
130 | @Override | |
131 | public Expression visitOr(Or node, AnalysisContext context) { | |
132 | Expression left = node.getLeft().accept(this, context); | |
133 | Expression right = node.getRight().accept(this, context); | |
134 | ||
135 |
1
1. visitOr : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitOr → KILLED |
return dsl.or(left, right); |
136 | } | |
137 | ||
138 | @Override | |
139 | public Expression visitXor(Xor node, AnalysisContext context) { | |
140 | Expression left = node.getLeft().accept(this, context); | |
141 | Expression right = node.getRight().accept(this, context); | |
142 | ||
143 |
1
1. visitXor : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitXor → KILLED |
return dsl.xor(left, right); |
144 | } | |
145 | ||
146 | @Override | |
147 | public Expression visitNot(Not node, AnalysisContext context) { | |
148 |
1
1. visitNot : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitNot → KILLED |
return dsl.not(node.getExpression().accept(this, context)); |
149 | } | |
150 | ||
151 | @Override | |
152 | public Expression visitAggregateFunction(AggregateFunction node, AnalysisContext context) { | |
153 | Optional<BuiltinFunctionName> builtinFunctionName = | |
154 | BuiltinFunctionName.ofAggregation(node.getFuncName()); | |
155 |
1
1. visitAggregateFunction : negated conditional → KILLED |
if (builtinFunctionName.isPresent()) { |
156 | ImmutableList.Builder<Expression> builder = ImmutableList.builder(); | |
157 | builder.add(node.getField().accept(this, context)); | |
158 | for (UnresolvedExpression arg : node.getArgList()) { | |
159 | builder.add(arg.accept(this, context)); | |
160 | } | |
161 | Aggregator aggregator = (Aggregator) repository.compile( | |
162 | builtinFunctionName.get().getName(), builder.build()); | |
163 | aggregator.distinct(node.getDistinct()); | |
164 |
1
1. visitAggregateFunction : negated conditional → KILLED |
if (node.condition() != null) { |
165 | aggregator.condition(analyze(node.condition(), context)); | |
166 | } | |
167 |
1
1. visitAggregateFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitAggregateFunction → KILLED |
return aggregator; |
168 | } else { | |
169 | throw new SemanticCheckException("Unsupported aggregation function " + node.getFuncName()); | |
170 | } | |
171 | } | |
172 | ||
173 | @Override | |
174 | public Expression visitRelevanceFieldList(RelevanceFieldList node, AnalysisContext context) { | |
175 |
1
1. visitRelevanceFieldList : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitRelevanceFieldList → KILLED |
return new LiteralExpression(ExprValueUtils.tupleValue( |
176 | ImmutableMap.copyOf(node.getFieldList()))); | |
177 | } | |
178 | ||
179 | @Override | |
180 | public Expression visitConstantFunction(ConstantFunction node, AnalysisContext context) { | |
181 | var valueName = node.getFuncName(); | |
182 |
1
1. visitConstantFunction : negated conditional → KILLED |
if (context.getConstantFunctionValues().containsKey(valueName)) { |
183 |
1
1. visitConstantFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitConstantFunction → KILLED |
return context.getConstantFunctionValues().get(valueName); |
184 | } | |
185 | ||
186 | var value = visitFunction(node, context); | |
187 | value = DSL.literal(value.valueOf()); | |
188 | context.getConstantFunctionValues().put(valueName, value); | |
189 |
1
1. visitConstantFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitConstantFunction → SURVIVED |
return value; |
190 | } | |
191 | ||
192 | @Override | |
193 | public Expression visitFunction(Function node, AnalysisContext context) { | |
194 | FunctionName functionName = FunctionName.of(node.getFuncName()); | |
195 | List<Expression> arguments = | |
196 | node.getFuncArgs().stream() | |
197 |
1
1. lambda$visitFunction$0 : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::lambda$visitFunction$0 → KILLED |
.map(unresolvedExpression -> analyze(unresolvedExpression, context)) |
198 | .collect(Collectors.toList()); | |
199 |
1
1. visitFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitFunction → KILLED |
return (Expression) repository.compile(functionName, arguments); |
200 | } | |
201 | ||
202 | @SuppressWarnings("unchecked") | |
203 | @Override | |
204 | public Expression visitWindowFunction(WindowFunction node, AnalysisContext context) { | |
205 | Expression expr = node.getFunction().accept(this, context); | |
206 | // Wrap regular aggregator by aggregate window function to adapt window operator use | |
207 |
1
1. visitWindowFunction : negated conditional → KILLED |
if (expr instanceof Aggregator) { |
208 |
1
1. visitWindowFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitWindowFunction → KILLED |
return new AggregateWindowFunction((Aggregator<AggregationState>) expr); |
209 | } | |
210 |
1
1. visitWindowFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitWindowFunction → KILLED |
return expr; |
211 | } | |
212 | ||
213 | @Override | |
214 | public Expression visitHighlightFunction(HighlightFunction node, AnalysisContext context) { | |
215 | Expression expr = node.getHighlightField().accept(this, context); | |
216 |
1
1. visitHighlightFunction : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitHighlightFunction → KILLED |
return new HighlightExpression(expr); |
217 | } | |
218 | ||
219 | @Override | |
220 | public Expression visitIn(In node, AnalysisContext context) { | |
221 |
1
1. visitIn : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitIn → KILLED |
return visitIn(node.getField(), node.getValueList(), context); |
222 | } | |
223 | ||
224 | private Expression visitIn( | |
225 | UnresolvedExpression field, List<UnresolvedExpression> valueList, AnalysisContext context) { | |
226 |
1
1. visitIn : negated conditional → KILLED |
if (valueList.size() == 1) { |
227 |
1
1. visitIn : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitIn → KILLED |
return visitCompare(new Compare("=", field, valueList.get(0)), context); |
228 |
2
1. visitIn : changed conditional boundary → SURVIVED 2. visitIn : negated conditional → KILLED |
} else if (valueList.size() > 1) { |
229 |
1
1. visitIn : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitIn → KILLED |
return dsl.or( |
230 | visitCompare(new Compare("=", field, valueList.get(0)), context), | |
231 | visitIn(field, valueList.subList(1, valueList.size()), context)); | |
232 | } else { | |
233 | throw new SemanticCheckException("Values in In clause should not be empty"); | |
234 | } | |
235 | } | |
236 | ||
237 | @Override | |
238 | public Expression visitCompare(Compare node, AnalysisContext context) { | |
239 | FunctionName functionName = FunctionName.of(node.getOperator()); | |
240 | Expression left = analyze(node.getLeft(), context); | |
241 | Expression right = analyze(node.getRight(), context); | |
242 |
1
1. visitCompare : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitCompare → KILLED |
return (Expression) |
243 | repository.compile(functionName, Arrays.asList(left, right)); | |
244 | } | |
245 | ||
246 | @Override | |
247 | public Expression visitCase(Case node, AnalysisContext context) { | |
248 | List<WhenClause> whens = new ArrayList<>(); | |
249 | for (When when : node.getWhenClauses()) { | |
250 |
1
1. visitCase : negated conditional → KILLED |
if (node.getCaseValue() == null) { |
251 | whens.add((WhenClause) analyze(when, context)); | |
252 | } else { | |
253 | // Merge case value and condition (compare value) into a single equal condition | |
254 | whens.add((WhenClause) analyze( | |
255 | new When( | |
256 | new Function("=", Arrays.asList(node.getCaseValue(), when.getCondition())), | |
257 | when.getResult() | |
258 | ), context)); | |
259 | } | |
260 | } | |
261 | ||
262 |
1
1. visitCase : negated conditional → KILLED |
Expression defaultResult = (node.getElseClause() == null) |
263 | ? null : analyze(node.getElseClause(), context); | |
264 | CaseClause caseClause = new CaseClause(whens, defaultResult); | |
265 | ||
266 | // To make this simple, require all result type same regardless of implicit convert | |
267 | // Make CaseClause return list so it can be used in error message in determined order | |
268 | List<ExprType> resultTypes = caseClause.allResultTypes(); | |
269 |
2
1. visitCase : changed conditional boundary → KILLED 2. visitCase : negated conditional → KILLED |
if (ImmutableSet.copyOf(resultTypes).size() > 1) { |
270 | throw new SemanticCheckException( | |
271 | "All result types of CASE clause must be the same, but found " + resultTypes); | |
272 | } | |
273 |
1
1. visitCase : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitCase → KILLED |
return caseClause; |
274 | } | |
275 | ||
276 | @Override | |
277 | public Expression visitWhen(When node, AnalysisContext context) { | |
278 |
1
1. visitWhen : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitWhen → KILLED |
return new WhenClause( |
279 | analyze(node.getCondition(), context), | |
280 | analyze(node.getResult(), context)); | |
281 | } | |
282 | ||
283 | @Override | |
284 | public Expression visitField(Field node, AnalysisContext context) { | |
285 | String attr = node.getField().toString(); | |
286 |
1
1. visitField : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitField → KILLED |
return visitIdentifier(attr, context); |
287 | } | |
288 | ||
289 | @Override | |
290 | public Expression visitAllFields(AllFields node, AnalysisContext context) { | |
291 | // Convert to string literal for argument in COUNT(*), because there is no difference between | |
292 | // COUNT(*) and COUNT(literal). For SELECT *, its select expression analyzer will expand * to | |
293 | // the right field name list by itself. | |
294 |
1
1. visitAllFields : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitAllFields → KILLED |
return DSL.literal("*"); |
295 | } | |
296 | ||
297 | @Override | |
298 | public Expression visitQualifiedName(QualifiedName node, AnalysisContext context) { | |
299 | QualifierAnalyzer qualifierAnalyzer = new QualifierAnalyzer(context); | |
300 |
1
1. visitQualifiedName : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitQualifiedName → KILLED |
return visitIdentifier(qualifierAnalyzer.unqualified(node), context); |
301 | } | |
302 | ||
303 | @Override | |
304 | public Expression visitSpan(Span node, AnalysisContext context) { | |
305 |
1
1. visitSpan : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitSpan → KILLED |
return new SpanExpression( |
306 | node.getField().accept(this, context), | |
307 | node.getValue().accept(this, context), | |
308 | node.getUnit()); | |
309 | } | |
310 | ||
311 | @Override | |
312 | public Expression visitUnresolvedArgument(UnresolvedArgument node, AnalysisContext context) { | |
313 |
1
1. visitUnresolvedArgument : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitUnresolvedArgument → KILLED |
return new NamedArgumentExpression(node.getArgName(), node.getValue().accept(this, context)); |
314 | } | |
315 | ||
316 | private Expression visitIdentifier(String ident, AnalysisContext context) { | |
317 | // ParseExpression will always override ReferenceExpression when ident conflicts | |
318 | for (NamedExpression expr : context.getNamedParseExpressions()) { | |
319 |
2
1. visitIdentifier : negated conditional → KILLED 2. visitIdentifier : negated conditional → KILLED |
if (expr.getNameOrAlias().equals(ident) && expr.getDelegated() instanceof ParseExpression) { |
320 |
1
1. visitIdentifier : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitIdentifier → KILLED |
return expr.getDelegated(); |
321 | } | |
322 | } | |
323 | ||
324 | TypeEnvironment typeEnv = context.peek(); | |
325 | ReferenceExpression ref = DSL.ref(ident, | |
326 | typeEnv.resolve(new Symbol(Namespace.FIELD_NAME, ident))); | |
327 | ||
328 | // Fall back to old engine too if type is not supported semantically | |
329 |
1
1. visitIdentifier : negated conditional → KILLED |
if (isTypeNotSupported(ref.type())) { |
330 | throw new SyntaxCheckException(String.format( | |
331 | "Identifier [%s] of type [%s] is not supported yet", ident, ref.type())); | |
332 | } | |
333 |
1
1. visitIdentifier : replaced return value with null for org/opensearch/sql/analysis/ExpressionAnalyzer::visitIdentifier → KILLED |
return ref; |
334 | } | |
335 | ||
336 | // Array type is not supporte yet. | |
337 | private boolean isTypeNotSupported(ExprType type) { | |
338 |
2
1. isTypeNotSupported : replaced boolean return with false for org/opensearch/sql/analysis/ExpressionAnalyzer::isTypeNotSupported → KILLED 2. isTypeNotSupported : replaced boolean return with true for org/opensearch/sql/analysis/ExpressionAnalyzer::isTypeNotSupported → KILLED |
return "array".equalsIgnoreCase(type.typeName()); |
339 | } | |
340 | ||
341 | } | |
Mutations | ||
82 |
1.1 |
|
93 |
1.1 |
|
98 |
1.1 |
|
106 |
1.1 |
|
111 |
1.1 |
|
119 |
1.1 |
|
127 |
1.1 |
|
135 |
1.1 |
|
143 |
1.1 |
|
148 |
1.1 |
|
155 |
1.1 |
|
164 |
1.1 |
|
167 |
1.1 |
|
175 |
1.1 |
|
182 |
1.1 |
|
183 |
1.1 |
|
189 |
1.1 |
|
197 |
1.1 |
|
199 |
1.1 |
|
207 |
1.1 |
|
208 |
1.1 |
|
210 |
1.1 |
|
216 |
1.1 |
|
221 |
1.1 |
|
226 |
1.1 |
|
227 |
1.1 |
|
228 |
1.1 2.2 |
|
229 |
1.1 |
|
242 |
1.1 |
|
250 |
1.1 |
|
262 |
1.1 |
|
269 |
1.1 2.2 |
|
273 |
1.1 |
|
278 |
1.1 |
|
286 |
1.1 |
|
294 |
1.1 |
|
300 |
1.1 |
|
305 |
1.1 |
|
313 |
1.1 |
|
319 |
1.1 2.2 |
|
320 |
1.1 |
|
329 |
1.1 |
|
333 |
1.1 |
|
338 |
1.1 2.2 |