diff --git a/plugins/query-dsl-calcite/README.md b/plugins/query-dsl-calcite/README.md index 07b603180d0a8..d6f49b488b215 100644 --- a/plugins/query-dsl-calcite/README.md +++ b/plugins/query-dsl-calcite/README.md @@ -61,12 +61,13 @@ RelNode (Calcite Logical Plan) ### Queries -| DSL Query | Calcite Representation | -|-----------|------------------------| -| `term` | `=($field, value)` — equality filter | -| `range` (gte, lte, gt, lt) | `AND(>=($field, min), <=($field, max))` — range filter | -| `bool` (must + filter) | `AND(condition1, condition2, ...)` — flattened conjunction | -| `match_all` | Skipped (boolean literal `TRUE`) | +| DSL Query | Calcite Representation | +|-----------|---------------------------------------------------------------------------| +| `term` | `=($field, value)` — equality filter | +| `range` (gte, lte, gt, lt) | `AND(>=($field, min), <=($field, max))` — range filter | +| `bool` (must + filter) | `AND(condition1, condition2, ...)` — flattened conjunction | +| `match_all` | Skipped (boolean literal `TRUE`) | +| `exists` | `IS NOT NULL($field)` — field existence check & boost not supported check | ### Aggregations diff --git a/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java b/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java index c9b5517206c54..e2b78f5e9934d 100644 --- a/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java +++ b/plugins/query-dsl-calcite/src/internalClusterTest/java/org/opensearch/dsl/DslLogicalPlanIntegrationIT.java @@ -795,4 +795,108 @@ public void testInvalidFieldError() throws Exception { errorMessage.contains("unknown")); } } + + /** + * Test: Exists query conversion. + * Verifies that an exists query is converted to a LogicalFilter with IS NOT NULL condition. + * + * DSL Query: + * { + * "query": { + * "exists": { + * "field": "description" + * } + * } + * } + * + * Expected Calcite Plan: + * LogicalFilter(condition=[IS NOT NULL($0)]) + * LogicalTableScan(table=[[test-exists-query]]) + */ + public void testExistsQueryConversion() throws Exception { + String indexName = "test-exists-query"; + String mapping = "{" + + "\"properties\": {" + + " \"description\": {\"type\": \"text\"}," + + " \"price\": {\"type\": \"long\"}" + + "}" + + "}"; + client().admin().indices().prepareCreate(indexName) + .setMapping(mapping) + .get(); + ensureGreen(indexName); + + SearchSourceBuilder searchSource = new SearchSourceBuilder(); + searchSource.query(QueryBuilders.existsQuery("description")); + + SearchResponse response = convertDsl(searchSource, indexName); + + assertNotNull("SearchResponse should not be null", response); + } + + /** + * Test: Exists query combined with bool query. + * Verifies that exists query works correctly within bool query context. + * + * DSL Query: + * { + * "query": { + * "bool": { + * "must": [ + * { "exists": { "field": "description" } } + * ], + * "filter": [ + * { "range": { "price": { "gte": 100 } } } + * ] + * } + * } + * } + * + * Expected Calcite Plan: + * LogicalFilter(condition=[AND(IS NOT NULL($0), >=($1, 100))]) + * LogicalTableScan(table=[[test-exists-bool-query]]) + */ + public void testExistsQueryWithBoolQuery() throws Exception { + String indexName = "test-exists-bool-query"; + String mapping = "{" + + "\"properties\": {" + + " \"description\": {\"type\": \"text\"}," + + " \"price\": {\"type\": \"long\"}" + + "}" + + "}"; + client().admin().indices().prepareCreate(indexName) + .setMapping(mapping) + .get(); + ensureGreen(indexName); + + SearchSourceBuilder searchSource = new SearchSourceBuilder(); + searchSource.query( + QueryBuilders.boolQuery() + .must(QueryBuilders.existsQuery("description")) + .filter(QueryBuilders.rangeQuery("price").gte(100)) + ); + + SearchResponse response = convertDsl(searchSource, indexName); + + assertNotNull("SearchResponse should not be null", response); + } + + public void testExistsQueryWithBoostNotSupported() throws Exception { + String indexName = "test-exists-boost"; + String mapping = "{" + + "\"properties\": {" + + " \"description\": {\"type\": \"text\"}" + + "}" + + "}"; + client().admin().indices().prepareCreate(indexName) + .setMapping(mapping) + .get(); + ensureGreen(indexName); + + SearchSourceBuilder searchSource = new SearchSourceBuilder(); + searchSource.query(QueryBuilders.existsQuery("description").boost(2.0f)); + + RuntimeException exception = expectThrows(RuntimeException.class, () -> convertDsl(searchSource, indexName)); + assertTrue(exception.getMessage().contains("boost is unsupported for Exists query type")); + } } diff --git a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/ExistsQueryTranslator.java b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/ExistsQueryTranslator.java new file mode 100644 index 0000000000000..5a38459272db2 --- /dev/null +++ b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/ExistsQueryTranslator.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dsl.query; + +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.opensearch.dsl.ConversionContext; +import org.opensearch.dsl.exception.ConversionException; +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.ExistsQueryBuilder; +import org.opensearch.index.query.QueryBuilder; + +/** + * Converts an {@link ExistsQueryBuilder} to a Calcite IS NOT NULL RexNode. + */ +public class ExistsQueryTranslator implements QueryTranslator { + + @Override + public Class getQueryType() { + return ExistsQueryBuilder.class; + } + + @Override + public RexNode convert(QueryBuilder query, ConversionContext ctx) throws ConversionException { + ctx.requireOperatorSupported(SqlStdOperatorTable.IS_NOT_NULL); + + ExistsQueryBuilder existsQuery = (ExistsQueryBuilder) query; + String fieldName = existsQuery.fieldName(); + float boost = existsQuery.boost(); + + RelDataTypeField field = ctx.getRowType().getField(fieldName, false, false); + if (field == null) { + throw new RuntimeException("Field '" + fieldName + "' not found in schema"); + } + if (boost != AbstractQueryBuilder.DEFAULT_BOOST) { + throw new RuntimeException("boost is unsupported for Exists query type"); + } + + RexNode fieldRef = ctx.getRexBuilder().makeInputRef(field.getType(), field.getIndex()); + return ctx.getRexBuilder().makeCall(SqlStdOperatorTable.IS_NOT_NULL, fieldRef); + } +} diff --git a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/QueryRegistryFactory.java b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/QueryRegistryFactory.java index 9e48b60dcead1..f65b4d6f4c5e2 100644 --- a/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/QueryRegistryFactory.java +++ b/plugins/query-dsl-calcite/src/main/java/org/opensearch/dsl/query/QueryRegistryFactory.java @@ -22,6 +22,7 @@ public static QueryRegistry create() { registry.register(new RangeQueryTranslator()); registry.register(new MatchAllQueryTranslator()); registry.register(new BoolQueryTranslator(registry)); + registry.register(new ExistsQueryTranslator()); return registry; } }