Skip to content

Commit adaaf49

Browse files
authored
Merge pull request #267 from Bit-Quill/dev-nested-where-clause-predicate-expression
Add Support for Nested Function Use In `WHERE` Clause
2 parents 8e5d766 + 6881b3a commit adaaf49

File tree

13 files changed

+461
-57
lines changed

13 files changed

+461
-57
lines changed

core/src/main/java/org/opensearch/sql/analysis/Analyzer.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
import org.opensearch.sql.common.antlr.SyntaxCheckException;
6464
import org.opensearch.sql.data.model.ExprMissingValue;
6565
import org.opensearch.sql.data.type.ExprCoreType;
66-
import org.opensearch.sql.data.type.ExprType;
6766
import org.opensearch.sql.datasource.DataSourceService;
6867
import org.opensearch.sql.exception.SemanticCheckException;
6968
import org.opensearch.sql.expression.DSL;
@@ -220,7 +219,6 @@ public LogicalPlan visitLimit(Limit node, AnalysisContext context) {
220219
public LogicalPlan visitFilter(Filter node, AnalysisContext context) {
221220
LogicalPlan child = node.getChild().get(0).accept(this, context);
222221
Expression condition = expressionAnalyzer.analyze(node.getCondition(), context);
223-
verifySupportsCondition(condition);
224222

225223
ExpressionReferenceOptimizer optimizer =
226224
new ExpressionReferenceOptimizer(expressionAnalyzer.getRepository(), child);
@@ -229,7 +227,7 @@ public LogicalPlan visitFilter(Filter node, AnalysisContext context) {
229227
}
230228

231229
/**
232-
* Ensure NESTED function is not used in WHERE, GROUP BY, and HAVING clauses.
230+
* Ensure NESTED function is not used in GROUP BY, and HAVING clauses.
233231
* Fallback to legacy engine. Can remove when support is added for NESTED function in WHERE,
234232
* GROUP BY, ORDER BY, and HAVING clauses.
235233
* @param condition : Filter condition

core/src/test/java/org/opensearch/sql/analysis/AnalyzerTest.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,29 +1041,6 @@ public void nested_group_by_clause_throws_syntax_exception() {
10411041
exception.getMessage());
10421042
}
10431043

1044-
/**
1045-
* Ensure Nested function falls back to legacy engine when used in WHERE clause.
1046-
* TODO Remove this test when support is added.
1047-
*/
1048-
@Test
1049-
public void nested_where_clause_throws_syntax_exception() {
1050-
SyntaxCheckException exception = assertThrows(SyntaxCheckException.class,
1051-
() -> analyze(
1052-
AstDSL.filter(
1053-
AstDSL.relation("schema"),
1054-
AstDSL.equalTo(
1055-
AstDSL.function("nested", qualifiedName("message", "info")),
1056-
AstDSL.stringLiteral("str")
1057-
)
1058-
)
1059-
)
1060-
);
1061-
assertEquals("Falling back to legacy engine. Nested function is not supported in WHERE,"
1062-
+ " GROUP BY, and HAVING clauses.",
1063-
exception.getMessage());
1064-
}
1065-
1066-
10671044
/**
10681045
* SELECT name, AVG(age) FROM test GROUP BY name.
10691046
*/

docs/user/dql/functions.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4456,6 +4456,17 @@ Example with ``field`` and ``path`` parameters::
44564456
+---------------------------------+
44574457

44584458

4459+
Example with ``field`` and ``path`` parameters in the SELECT and WHERE clause::
4460+
4461+
os> SELECT nested(message.info, message) FROM nested WHERE nested(message.info, message) = 'b';
4462+
fetched rows / total rows = 1/1
4463+
+---------------------------------+
4464+
| nested(message.info, message) |
4465+
|---------------------------------|
4466+
| b |
4467+
+---------------------------------+
4468+
4469+
44594470
System Functions
44604471
================
44614472

integ-test/src/test/java/org/opensearch/sql/sql/NestedIT.java

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,6 @@ public void nested_function_mixed_with_non_nested_type_test() {
170170
rows("zz", "a"));
171171
}
172172

173-
@Test
174-
public void nested_function_with_where_clause() {
175-
String query =
176-
"SELECT nested(message.info) FROM " + TEST_INDEX_NESTED_TYPE + " WHERE nested(message.info) = 'a'";
177-
JSONObject result = executeJdbcRequest(query);
178-
179-
assertEquals(2, result.getInt("total"));
180-
verifyDataRows(result,
181-
rows("a"),
182-
rows("a"));
183-
}
184-
185173
@Test
186174
public void nested_function_with_order_by_clause() {
187175
String query =
@@ -313,4 +301,69 @@ public void nested_missing_path_argument() {
313301
"}"
314302
));
315303
}
304+
305+
@Test
306+
public void test_nested_where_with_and_conditional() {
307+
String query = "SELECT nested(message.info), nested(message.author) FROM " + TEST_INDEX_NESTED_TYPE
308+
+ " WHERE nested(message, message.info = 'a' AND message.author = 'e')";
309+
JSONObject result = executeJdbcRequest(query);
310+
assertEquals(1, result.getInt("total"));
311+
verifyDataRows(result, rows("a", "e"));
312+
}
313+
314+
@Test
315+
public void test_nested_in_select_and_where_as_predicate_expression() {
316+
String query = "SELECT nested(message.info) FROM " + TEST_INDEX_NESTED_TYPE
317+
+ " WHERE nested(message.info) = 'a'";
318+
JSONObject result = executeJdbcRequest(query);
319+
assertEquals(3, result.getInt("total"));
320+
verifyDataRows(
321+
result,
322+
rows("a"),
323+
rows("c"),
324+
rows("a")
325+
);
326+
}
327+
328+
@Test
329+
public void test_nested_in_where_as_predicate_expression() {
330+
String query = "SELECT message.info FROM " + TEST_INDEX_NESTED_TYPE
331+
+ " WHERE nested(message.info) = 'a'";
332+
JSONObject result = executeJdbcRequest(query);
333+
assertEquals(2, result.getInt("total"));
334+
// Only first index of array is returned. Second index has 'a'
335+
verifyDataRows(result, rows("a"), rows("c"));
336+
}
337+
338+
@Test
339+
public void test_nested_in_where_as_predicate_expression_with_like() {
340+
String query = "SELECT message.info FROM " + TEST_INDEX_NESTED_TYPE
341+
+ " WHERE nested(message.info) LIKE 'a'";
342+
JSONObject result = executeJdbcRequest(query);
343+
assertEquals(2, result.getInt("total"));
344+
// Only first index of array is returned. Second index has 'a'
345+
verifyDataRows(result, rows("a"), rows("c"));
346+
}
347+
348+
@Test
349+
public void test_nested_in_where_as_predicate_expression_with_multiple_conditions() {
350+
String query = "SELECT message.info, comment.data, message.dayOfWeek FROM " + TEST_INDEX_NESTED_TYPE
351+
+ " WHERE nested(message.info) = 'zz' OR nested(comment.data) = 'ab' AND nested(message.dayOfWeek) >= 4";
352+
JSONObject result = executeJdbcRequest(query);
353+
assertEquals(2, result.getInt("total"));
354+
verifyDataRows(
355+
result,
356+
rows("c", "ab", 4),
357+
rows("zz", "aa", 6)
358+
);
359+
}
360+
361+
@Test
362+
public void test_nested_in_where_as_predicate_expression_with_relevance_query() {
363+
String query = "SELECT comment.likes, someField FROM " + TEST_INDEX_NESTED_TYPE
364+
+ " WHERE nested(comment.likes) = 10 AND match(someField, 'a')";
365+
JSONObject result = executeJdbcRequest(query);
366+
assertEquals(1, result.getInt("total"));
367+
verifyDataRows(result, rows(10, "a"));
368+
}
316369
}

opensearch/src/main/java/org/opensearch/sql/opensearch/request/OpenSearchRequestBuilder.java

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88

99
import static java.util.stream.Collectors.mapping;
1010
import static java.util.stream.Collectors.toList;
11-
import static org.opensearch.index.query.QueryBuilders.boolQuery;
1211
import static org.opensearch.index.query.QueryBuilders.matchAllQuery;
1312
import static org.opensearch.index.query.QueryBuilders.nestedQuery;
1413
import static org.opensearch.search.sort.FieldSortBuilder.DOC_FIELD_NAME;
1514
import static org.opensearch.search.sort.SortOrder.ASC;
1615

16+
import java.util.ArrayList;
1717
import java.util.Arrays;
18+
import java.util.Collection;
1819
import java.util.List;
1920
import java.util.Map;
2021
import java.util.Set;
22+
import java.util.function.Supplier;
2123
import java.util.stream.Collectors;
24+
import java.util.stream.Stream;
2225
import lombok.EqualsAndHashCode;
2326
import lombok.Getter;
2427
import lombok.ToString;
@@ -44,7 +47,6 @@
4447
import org.opensearch.sql.opensearch.data.type.OpenSearchDataType;
4548
import org.opensearch.sql.opensearch.data.value.OpenSearchExprValueFactory;
4649
import org.opensearch.sql.opensearch.response.agg.OpenSearchAggregationResponseParser;
47-
import org.opensearch.sql.planner.logical.LogicalNested;
4850

4951
/**
5052
* OpenSearch search request builder.
@@ -257,13 +259,33 @@ private boolean isSortByDocOnly() {
257259
*/
258260
public void pushDownNested(List<Map<String, ReferenceExpression>> nestedArgs) {
259261
initBoolQueryFilter();
262+
List<NestedQueryBuilder> nestedQueries = extractNestedQueries(query());
260263
groupFieldNamesByPath(nestedArgs).forEach(
261-
(path, fieldNames) -> buildInnerHit(
262-
fieldNames, createEmptyNestedQuery(path)
263-
)
264+
(path, fieldNames) ->
265+
buildInnerHit(fieldNames, findNestedQueryWithSamePath(nestedQueries, path))
264266
);
265267
}
266268

269+
/**
270+
* InnerHit must be added to the NestedQueryBuilder. We need to extract
271+
* the nested queries currently in the query if there is already a filter
272+
* push down with nested query.
273+
* @param query : current query.
274+
* @return : grouped nested queries currently in query.
275+
*/
276+
private List<NestedQueryBuilder> extractNestedQueries(QueryBuilder query) {
277+
List<NestedQueryBuilder> result = new ArrayList<>();
278+
if (query instanceof NestedQueryBuilder) {
279+
result.add((NestedQueryBuilder) query);
280+
} else if (query instanceof BoolQueryBuilder) {
281+
BoolQueryBuilder boolQ = (BoolQueryBuilder) query;
282+
Stream.of(boolQ.filter(), boolQ.must(), boolQ.should())
283+
.flatMap(Collection::stream)
284+
.forEach(q -> result.addAll(extractNestedQueries(q)));
285+
}
286+
return result;
287+
}
288+
267289
/**
268290
* Initialize bool query for push down.
269291
*/
@@ -307,13 +329,41 @@ private void buildInnerHit(List<String> paths, NestedQueryBuilder query) {
307329
));
308330
}
309331

332+
/**
333+
* We need to group nested queries with same path for adding new fields with same path of
334+
* inner hits. If we try to add additional inner hits with same path we get an OS error.
335+
* @param nestedQueries Current list of nested queries in query.
336+
* @param path path comparing with current nested queries.
337+
* @return Query with same path or new empty nested query.
338+
*/
339+
private NestedQueryBuilder findNestedQueryWithSamePath(
340+
List<NestedQueryBuilder> nestedQueries, String path
341+
) {
342+
return nestedQueries.stream()
343+
.filter(query -> isSamePath(path, query))
344+
.findAny()
345+
.orElseGet(createEmptyNestedQuery(path));
346+
}
347+
348+
/**
349+
* Check if is nested query is of the same path value.
350+
* @param path Value of path to compare with nested query.
351+
* @param query nested query builder to compare with path.
352+
* @return true if nested query has same path.
353+
*/
354+
private boolean isSamePath(String path, NestedQueryBuilder query) {
355+
return nestedQuery(path, query.query(), query.scoreMode()).equals(query);
356+
}
357+
310358
/**
311359
* Create a nested query with match all filter to place inner hits.
312360
*/
313-
private NestedQueryBuilder createEmptyNestedQuery(String path) {
314-
NestedQueryBuilder nestedQuery = nestedQuery(path, matchAllQuery(), ScoreMode.None);
315-
((BoolQueryBuilder) query().filter().get(0)).must(nestedQuery);
316-
return nestedQuery;
361+
private Supplier<NestedQueryBuilder> createEmptyNestedQuery(String path) {
362+
return () -> {
363+
NestedQueryBuilder nestedQuery = nestedQuery(path, matchAllQuery(), ScoreMode.None);
364+
((BoolQueryBuilder) query().filter().get(0)).must(nestedQuery);
365+
return nestedQuery;
366+
};
317367
}
318368

319369
/**

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilder.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,23 @@
1414
import java.util.Map;
1515
import java.util.function.BiFunction;
1616
import lombok.RequiredArgsConstructor;
17+
import org.apache.lucene.search.join.ScoreMode;
1718
import org.opensearch.index.query.BoolQueryBuilder;
1819
import org.opensearch.index.query.QueryBuilder;
1920
import org.opensearch.index.query.QueryBuilders;
2021
import org.opensearch.index.query.ScriptQueryBuilder;
2122
import org.opensearch.script.Script;
23+
import org.opensearch.sql.ast.expression.Function;
24+
import org.opensearch.sql.common.antlr.SyntaxCheckException;
2225
import org.opensearch.sql.expression.Expression;
2326
import org.opensearch.sql.expression.ExpressionNodeVisitor;
2427
import org.opensearch.sql.expression.FunctionExpression;
28+
import org.opensearch.sql.expression.ReferenceExpression;
2529
import org.opensearch.sql.expression.function.BuiltinFunctionName;
2630
import org.opensearch.sql.expression.function.FunctionName;
2731
import org.opensearch.sql.opensearch.storage.script.filter.lucene.LikeQuery;
2832
import org.opensearch.sql.opensearch.storage.script.filter.lucene.LuceneQuery;
33+
import org.opensearch.sql.opensearch.storage.script.filter.lucene.NestedQuery;
2934
import org.opensearch.sql.opensearch.storage.script.filter.lucene.RangeQuery;
3035
import org.opensearch.sql.opensearch.storage.script.filter.lucene.RangeQuery.Comparison;
3136
import org.opensearch.sql.opensearch.storage.script.filter.lucene.TermQuery;
@@ -75,6 +80,7 @@ public class FilterQueryBuilder extends ExpressionNodeVisitor<QueryBuilder, Obje
7580
.put(BuiltinFunctionName.MATCH_PHRASE_PREFIX.getName(), new MatchPhrasePrefixQuery())
7681
.put(BuiltinFunctionName.WILDCARD_QUERY.getName(), new WildcardQuery())
7782
.put(BuiltinFunctionName.WILDCARDQUERY.getName(), new WildcardQuery())
83+
.put(BuiltinFunctionName.NESTED.getName(), new NestedQuery())
7884
.build();
7985

8086
/**
@@ -96,10 +102,20 @@ public QueryBuilder visitFunction(FunctionExpression func, Object context) {
96102
return buildBoolQuery(func, context, BoolQueryBuilder::should);
97103
case "not":
98104
return buildBoolQuery(func, context, BoolQueryBuilder::mustNot);
105+
case "nested":
106+
// TODO Fill in case when adding support for syntax - nested(path, condition)
107+
throw new SyntaxCheckException(
108+
"Invalid syntax used for nested function in WHERE clause: "
109+
+ "nested(field | field, path) OPERATOR LITERAL"
110+
);
99111
default: {
100112
LuceneQuery query = luceneQueries.get(name);
101113
if (query != null && query.canSupport(func)) {
102114
return query.build(func);
115+
} else if (query != null && query.isNestedPredicate(func)) {
116+
NestedQuery nestedQuery = (NestedQuery) luceneQueries.get(
117+
((FunctionExpression)func.getArguments().get(0)).getFunctionName());
118+
return nestedQuery.buildNested(func, query);
103119
}
104120
return buildScriptQuery(func);
105121
}

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,15 @@
99
import org.opensearch.index.query.QueryBuilders;
1010
import org.opensearch.index.query.WildcardQueryBuilder;
1111
import org.opensearch.sql.data.model.ExprValue;
12-
import org.opensearch.sql.expression.Expression;
13-
import org.opensearch.sql.expression.FunctionExpression;
14-
import org.opensearch.sql.expression.ReferenceExpression;
12+
import org.opensearch.sql.data.type.ExprType;
1513
import org.opensearch.sql.opensearch.data.type.OpenSearchTextType;
1614
import org.opensearch.sql.opensearch.storage.script.StringUtils;
1715

1816
public class LikeQuery extends LuceneQuery {
1917
@Override
20-
public QueryBuilder build(FunctionExpression func) {
21-
ReferenceExpression ref = (ReferenceExpression) func.getArguments().get(0);
22-
String field = OpenSearchTextType.convertTextToKeyword(ref.getAttr(), ref.type());
23-
Expression expr = func.getArguments().get(1);
24-
ExprValue literalValue = expr.valueOf();
25-
return createBuilder(field, literalValue.stringValue());
18+
public QueryBuilder doBuild(String fieldName, ExprType fieldType, ExprValue literal) {
19+
String field = OpenSearchTextType.convertTextToKeyword(fieldName, fieldType);
20+
return createBuilder(field, literal.stringValue());
2621
}
2722

2823
/**

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.opensearch.sql.expression.ReferenceExpression;
3333
import org.opensearch.sql.expression.function.BuiltinFunctionName;
3434
import org.opensearch.sql.expression.function.FunctionName;
35-
import org.opensearch.sql.opensearch.data.type.OpenSearchTextType;
3635

3736
/**
3837
* Lucene query abstraction that builds Lucene query from function expression.
@@ -56,6 +55,19 @@ public boolean canSupport(FunctionExpression func) {
5655
|| isMultiParameterQuery(func);
5756
}
5857

58+
/**
59+
* Check if predicate expression has nested function on left side of predicate expression.
60+
* Validation for right side being a `LiteralExpression` is done in NestedQuery.
61+
* @param func function.
62+
* @return return true if function has supported nested function expression.
63+
*/
64+
public boolean isNestedPredicate(FunctionExpression func) {
65+
return ((func.getArguments().get(0) instanceof FunctionExpression
66+
&& ((FunctionExpression)func.getArguments().get(0))
67+
.getFunctionName().getFunctionName().equalsIgnoreCase(BuiltinFunctionName.NESTED.name()))
68+
);
69+
}
70+
5971
/**
6072
* Check if the function expression has multiple named argument expressions as the parameters.
6173
*

0 commit comments

Comments
 (0)