diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoConverter.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoConverter.java new file mode 100644 index 0000000000000..aedeb865f9493 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoConverter.java @@ -0,0 +1,41 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter; + +/** + * Converter for Ids queries. + * This class implements the QueryBuilderProtoConverter interface to provide Ids query support + * for the gRPC transport module. + */ +public class IdsQueryBuilderProtoConverter implements QueryBuilderProtoConverter { + + /** + * Constructs a new IdsQueryBuilderProtoConverter. + */ + public IdsQueryBuilderProtoConverter() { + // Default constructor + } + + @Override + public QueryContainer.QueryContainerCase getHandledQueryCase() { + return QueryContainer.QueryContainerCase.IDS; + } + + @Override + public QueryBuilder fromProto(QueryContainer queryContainer) { + if (queryContainer == null || !queryContainer.hasIds()) { + throw new IllegalArgumentException("QueryContainer does not contain an Ids query"); + } + + return IdsQueryBuilderProtoUtils.fromProto(queryContainer.getIds()); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoUtils.java new file mode 100644 index 0000000000000..e2cf5b0711d82 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoUtils.java @@ -0,0 +1,58 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.IdsQueryBuilder; +import org.opensearch.protobufs.IdsQuery; + +/** + * Utility class for converting IdsQuery Protocol Buffers to OpenSearch objects. + * This class provides methods to transform Protocol Buffer representations of ids queries + * into their corresponding OpenSearch IdsQueryBuilder implementations for search operations. + */ +class IdsQueryBuilderProtoUtils { + + private IdsQueryBuilderProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer IdsQuery to an OpenSearch IdsQueryBuilder. + * Similar to {@link IdsQueryBuilder#fromXContent(org.opensearch.core.xcontent.XContentParser)}, this method + * parses the Protocol Buffer representation and creates a properly configured + * IdsQueryBuilder with the appropriate ids, boost, and query name. + * + * @param idsQueryProto The Protocol Buffer IdsQuery object + * @return A configured IdsQueryBuilder instance + */ + static IdsQueryBuilder fromProto(IdsQuery idsQueryProto) { + // Create IdsQueryBuilder + IdsQueryBuilder idsQuery = new IdsQueryBuilder(); + + // Process name (only set when present) + if (idsQueryProto.hasXName()) { + idsQuery.queryName(idsQueryProto.getXName()); + } + + // Process boost (only set when present) + if (idsQueryProto.hasBoost()) { + idsQuery.boost(idsQueryProto.getBoost()); + } + + // Process values (ids) + if (idsQueryProto.getValuesCount() > 0) { + String[] ids = new String[idsQueryProto.getValuesCount()]; + for (int i = 0; i < idsQueryProto.getValuesCount(); i++) { + ids[i] = idsQueryProto.getValues(i); + } + idsQuery.addIds(ids); + } + + return idsQuery; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java index cb3270f4035c6..1eea70fb41cbc 100644 --- a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java @@ -58,6 +58,9 @@ protected void registerBuiltInConverters() { delegate.registerConverter(new GeoBoundingBoxQueryBuilderProtoConverter()); delegate.registerConverter(new GeoDistanceQueryBuilderProtoConverter()); delegate.registerConverter(new NestedQueryBuilderProtoConverter()); + delegate.registerConverter(new IdsQueryBuilderProtoConverter()); + delegate.registerConverter(new RangeQueryBuilderProtoConverter()); + delegate.registerConverter(new TermsSetQueryBuilderProtoConverter()); // Set the registry on all converters so they can access each other delegate.setRegistryOnAllConverters(this); diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoConverter.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoConverter.java new file mode 100644 index 0000000000000..37d8066afcc96 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoConverter.java @@ -0,0 +1,43 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter; + +/** + * Protocol Buffer converter for RangeQuery. + * This converter handles the transformation of Protocol Buffer RangeQuery objects + * into OpenSearch RangeQueryBuilder instances for range search operations. + * + * Range queries are handled as a map where the key is the field name and the value is the RangeQuery. + */ +public class RangeQueryBuilderProtoConverter implements QueryBuilderProtoConverter { + + /** + * Default constructor for RangeQueryBuilderProtoConverter. + */ + public RangeQueryBuilderProtoConverter() { + // Default constructor + } + + @Override + public QueryContainer.QueryContainerCase getHandledQueryCase() { + return QueryContainer.QueryContainerCase.RANGE; + } + + @Override + public QueryBuilder fromProto(QueryContainer queryContainer) { + if (queryContainer == null || queryContainer.getQueryContainerCase() != QueryContainer.QueryContainerCase.RANGE) { + throw new IllegalArgumentException("QueryContainer must contain a RangeQuery"); + } + + return RangeQueryBuilderProtoUtils.fromProto(queryContainer.getRange()); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoUtils.java new file mode 100644 index 0000000000000..ea0f67037d541 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoUtils.java @@ -0,0 +1,280 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.RangeQueryBuilder; +import org.opensearch.protobufs.DateRangeQuery; +import org.opensearch.protobufs.DateRangeQueryAllOfFrom; +import org.opensearch.protobufs.DateRangeQueryAllOfTo; +import org.opensearch.protobufs.NumberRangeQuery; +import org.opensearch.protobufs.NumberRangeQueryAllOfFrom; +import org.opensearch.protobufs.NumberRangeQueryAllOfTo; +import org.opensearch.protobufs.RangeQuery; +import org.opensearch.protobufs.RangeRelation; + +/** + * Utility class for converting RangeQuery Protocol Buffers to OpenSearch objects. + * This class provides methods to transform Protocol Buffer representations of range queries + * into their corresponding OpenSearch RangeQueryBuilder implementations for search operations. + */ +class RangeQueryBuilderProtoUtils { + + private RangeQueryBuilderProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer RangeQuery to an OpenSearch RangeQueryBuilder. + * Similar to {@link RangeQueryBuilder#fromXContent(org.opensearch.core.xcontent.XContentParser)}, this method + * parses the Protocol Buffer representation and creates a properly configured + * RangeQueryBuilder with the appropriate range parameters, format, time zone, + * relation, boost, and query name. + * + * @param rangeQueryProto The Protocol Buffer RangeQuery object + * @return A configured RangeQueryBuilder instance + * @throws IllegalArgumentException if no valid range query is found + */ + static RangeQueryBuilder fromProto(RangeQuery rangeQueryProto) { + if (rangeQueryProto == null) { + throw new IllegalArgumentException("RangeQuery cannot be null"); + } + + if (rangeQueryProto.hasDateRangeQuery()) { + return fromDateRangeQuery(rangeQueryProto.getDateRangeQuery()); + } else if (rangeQueryProto.hasNumberRangeQuery()) { + return fromNumberRangeQuery(rangeQueryProto.getNumberRangeQuery()); + } else { + throw new IllegalArgumentException("RangeQuery must contain either DateRangeQuery or NumberRangeQuery"); + } + } + + /** + * Converts a DateRangeQuery to a RangeQueryBuilder. + */ + private static RangeQueryBuilder fromDateRangeQuery(DateRangeQuery dateRangeQuery) { + // Extract field name from the protobuf + String fieldName = dateRangeQuery.getField(); + if (fieldName.isEmpty()) { + throw new IllegalArgumentException("Field name cannot be null or empty for range query"); + } + + RangeQueryBuilder rangeQuery = new RangeQueryBuilder(fieldName); + + String queryName = dateRangeQuery.hasXName() ? dateRangeQuery.getXName() : null; + float boost = dateRangeQuery.hasBoost() ? dateRangeQuery.getBoost() : AbstractQueryBuilder.DEFAULT_BOOST; + String format = dateRangeQuery.hasFormat() ? dateRangeQuery.getFormat() : null; + String timeZone = dateRangeQuery.getTimeZone().isEmpty() ? null : dateRangeQuery.getTimeZone(); + String relation = null; + + boolean includeLower = RangeQueryBuilder.DEFAULT_INCLUDE_LOWER; + boolean includeUpper = RangeQueryBuilder.DEFAULT_INCLUDE_UPPER; + Object from = null; + Object to = null; + + if (dateRangeQuery.hasFrom()) { + DateRangeQueryAllOfFrom fromObj = dateRangeQuery.getFrom(); + if (fromObj.hasString()) { + from = fromObj.getString(); + } else if (fromObj.hasNullValue()) { + from = null; + } + } + + if (dateRangeQuery.hasTo()) { + DateRangeQueryAllOfTo toObj = dateRangeQuery.getTo(); + if (toObj.hasString()) { + to = toObj.getString(); + } else if (toObj.hasNullValue()) { + to = null; + } + } + + if (dateRangeQuery.hasIncludeLower()) { + includeLower = dateRangeQuery.getIncludeLower(); + } + + if (dateRangeQuery.hasIncludeUpper()) { + includeUpper = dateRangeQuery.getIncludeUpper(); + } + + if (dateRangeQuery.hasGt()) { + from = dateRangeQuery.getGt(); + includeLower = false; + } + + if (dateRangeQuery.hasGte()) { + from = dateRangeQuery.getGte(); + includeLower = true; + } + + if (dateRangeQuery.hasLt()) { + to = dateRangeQuery.getLt(); + includeUpper = false; + } + + if (dateRangeQuery.hasLte()) { + to = dateRangeQuery.getLte(); + includeUpper = true; + } + + if (from != null) { + rangeQuery.from(from); + } + if (to != null) { + rangeQuery.to(to); + } + + rangeQuery.includeLower(includeLower); + rangeQuery.includeUpper(includeUpper); + + if (dateRangeQuery.hasRelation()) { + relation = parseRangeRelation(dateRangeQuery.getRelation()); + } + + if (format != null) { + rangeQuery.format(format); + } + + if (timeZone != null) { + rangeQuery.timeZone(timeZone); + } + + if (relation != null) { + rangeQuery.relation(relation); + } + + rangeQuery.boost(boost); + + if (queryName != null) { + rangeQuery.queryName(queryName); + } + + return rangeQuery; + } + + /** + * Converts a NumberRangeQuery to a RangeQueryBuilder. + */ + private static RangeQueryBuilder fromNumberRangeQuery(NumberRangeQuery numberRangeQuery) { + // Extract field name from the protobuf + String fieldName = numberRangeQuery.getField(); + if (fieldName.isEmpty()) { + throw new IllegalArgumentException("Field name cannot be null or empty for range query"); + } + + RangeQueryBuilder rangeQuery = new RangeQueryBuilder(fieldName); + + String queryName = numberRangeQuery.hasXName() ? numberRangeQuery.getXName() : null; + float boost = numberRangeQuery.hasBoost() ? numberRangeQuery.getBoost() : AbstractQueryBuilder.DEFAULT_BOOST; + String relation = null; + + boolean includeLower = RangeQueryBuilder.DEFAULT_INCLUDE_LOWER; + boolean includeUpper = RangeQueryBuilder.DEFAULT_INCLUDE_UPPER; + Object from = null; + Object to = null; + + if (numberRangeQuery.hasFrom()) { + NumberRangeQueryAllOfFrom fromObj = numberRangeQuery.getFrom(); + if (fromObj.hasDouble()) { + from = fromObj.getDouble(); + } else if (fromObj.hasString()) { + from = fromObj.getString(); + } else if (fromObj.hasNullValue()) { + from = null; + } + } + + if (numberRangeQuery.hasTo()) { + NumberRangeQueryAllOfTo toObj = numberRangeQuery.getTo(); + if (toObj.hasDouble()) { + to = toObj.getDouble(); + } else if (toObj.hasString()) { + to = toObj.getString(); + } else if (toObj.hasNullValue()) { + to = null; + } + } + + if (numberRangeQuery.hasIncludeLower()) { + includeLower = numberRangeQuery.getIncludeLower(); + } + + if (numberRangeQuery.hasIncludeUpper()) { + includeUpper = numberRangeQuery.getIncludeUpper(); + } + + if (numberRangeQuery.hasGt()) { + from = numberRangeQuery.getGt(); + includeLower = false; + } + + if (numberRangeQuery.hasGte()) { + from = numberRangeQuery.getGte(); + includeLower = true; + } + + if (numberRangeQuery.hasLt()) { + to = numberRangeQuery.getLt(); + includeUpper = false; + } + if (numberRangeQuery.hasLte()) { + to = numberRangeQuery.getLte(); + includeUpper = true; + } + + if (from != null) { + rangeQuery.from(from); + } + if (to != null) { + rangeQuery.to(to); + } + + rangeQuery.includeLower(includeLower); + rangeQuery.includeUpper(includeUpper); + + if (numberRangeQuery.hasRelation()) { + relation = parseRangeRelation(numberRangeQuery.getRelation()); + } + + if (relation != null) { + rangeQuery.relation(relation); + } + + rangeQuery.boost(boost); + + if (queryName != null) { + rangeQuery.queryName(queryName); + } + + return rangeQuery; + } + + /** + * Parses RangeRelation enum to string. + * + * @param rangeRelation The RangeRelation enum value + * @return The corresponding string representation, or null if unsupported + */ + private static String parseRangeRelation(RangeRelation rangeRelation) { + if (rangeRelation == null) { + return null; + } + + switch (rangeRelation) { + case RANGE_RELATION_CONTAINS: + return "CONTAINS"; + case RANGE_RELATION_INTERSECTS: + return "INTERSECTS"; + case RANGE_RELATION_WITHIN: + return "WITHIN"; + default: + return null; + } + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoConverter.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoConverter.java new file mode 100644 index 0000000000000..7801faa90bf56 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoConverter.java @@ -0,0 +1,41 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter; + +/** + * Converter for TermsSet queries. + * This class implements the QueryBuilderProtoConverter interface to provide TermsSet query support + * for the gRPC transport module. + */ +public class TermsSetQueryBuilderProtoConverter implements QueryBuilderProtoConverter { + + /** + * Constructs a new TermsSetQueryBuilderProtoConverter. + */ + public TermsSetQueryBuilderProtoConverter() { + // Default constructor + } + + @Override + public QueryContainer.QueryContainerCase getHandledQueryCase() { + return QueryContainer.QueryContainerCase.TERMS_SET; + } + + @Override + public QueryBuilder fromProto(QueryContainer queryContainer) { + if (queryContainer == null || !queryContainer.hasTermsSet()) { + throw new IllegalArgumentException("QueryContainer does not contain a TermsSet query"); + } + + return TermsSetQueryBuilderProtoUtils.fromProto(queryContainer.getTermsSet()); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoUtils.java new file mode 100644 index 0000000000000..97468021c7e65 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoUtils.java @@ -0,0 +1,91 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.TermsSetQueryBuilder; +import org.opensearch.script.Script; +import org.opensearch.transport.grpc.proto.request.common.ScriptProtoUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for converting TermsSet query Protocol Buffers to OpenSearch objects. + * This class provides methods to transform Protocol Buffer representations of terms_set queries + * into their corresponding OpenSearch TermsSetQueryBuilder implementations for search operations. + */ +class TermsSetQueryBuilderProtoUtils { + + private TermsSetQueryBuilderProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer TermsSetQuery to an OpenSearch TermsSetQueryBuilder. + * Similar to {@link TermsSetQueryBuilder#fromXContent(XContentParser)}, this method + * parses the Protocol Buffer representation and creates a properly configured + * TermsSetQueryBuilder with the appropriate field name, terms, boost, query name, + * and minimum should match settings. + * + * @param termsSetQueryProto The Protocol Buffer TermsSetQuery object + * @return A configured TermsSetQueryBuilder instance + * @throws IllegalArgumentException if the terms_set query is invalid or missing required fields + */ + static TermsSetQueryBuilder fromProto(org.opensearch.protobufs.TermsSetQuery termsSetQueryProto) { + if (termsSetQueryProto == null) { + throw new IllegalArgumentException("TermsSetQuery must not be null"); + } + + if (termsSetQueryProto.getField() == null || termsSetQueryProto.getField().isEmpty()) { + throw new IllegalArgumentException("Field name is required for TermsSetQuery"); + } + + if (termsSetQueryProto.getTermsCount() == 0) { + throw new IllegalArgumentException("At least one term is required for TermsSetQuery"); + } + + String fieldName = termsSetQueryProto.getField(); + List values = new ArrayList<>(termsSetQueryProto.getTermsList()); + String minimumShouldMatchField = null; + Script minimumShouldMatchScript = null; + String queryName = null; + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + + if (termsSetQueryProto.hasBoost()) { + boost = termsSetQueryProto.getBoost(); + } + + if (termsSetQueryProto.hasXName()) { + queryName = termsSetQueryProto.getXName(); + } + + if (termsSetQueryProto.hasMinimumShouldMatchField()) { + minimumShouldMatchField = termsSetQueryProto.getMinimumShouldMatchField(); + } + + if (termsSetQueryProto.hasMinimumShouldMatchScript()) { + minimumShouldMatchScript = ScriptProtoUtils.parseFromProtoRequest(termsSetQueryProto.getMinimumShouldMatchScript()); + } + + TermsSetQueryBuilder queryBuilder = new TermsSetQueryBuilder(fieldName, values); + + queryBuilder.boost(boost); + queryBuilder.queryName(queryName); + + if (minimumShouldMatchField != null) { + queryBuilder.setMinimumShouldMatchField(minimumShouldMatchField); + } + if (minimumShouldMatchScript != null) { + queryBuilder.setMinimumShouldMatchScript(minimumShouldMatchScript); + } + + return queryBuilder; + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoConverterTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoConverterTests.java new file mode 100644 index 0000000000000..cdcde100ad1b5 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoConverterTests.java @@ -0,0 +1,132 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.IdsQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.IdsQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.test.OpenSearchTestCase; + +public class IdsQueryBuilderProtoConverterTests extends OpenSearchTestCase { + + private IdsQueryBuilderProtoConverter converter; + + @Override + public void setUp() throws Exception { + super.setUp(); + converter = new IdsQueryBuilderProtoConverter(); + QueryBuilderProtoTestUtils.setupRegistry(); + } + + public void testGetHandledQueryCase() { + assertEquals(QueryContainer.QueryContainerCase.IDS, converter.getHandledQueryCase()); + } + + public void testFromProtoWithValidIdsQuery() { + // Create a valid IdsQuery + IdsQuery idsQuery = IdsQuery.newBuilder().setXName("test_query").setBoost(1.5f).addValues("doc1").addValues("doc2").build(); + + // Create QueryContainer with IdsQuery + QueryContainer queryContainer = QueryContainer.newBuilder().setIds(idsQuery).build(); + + // Call the method under test + QueryBuilder result = converter.fromProto(queryContainer); + + // Verify the result + assertNotNull(result); + assertTrue(result instanceof IdsQueryBuilder); + + IdsQueryBuilder idsQueryBuilder = (IdsQueryBuilder) result; + assertEquals("test_query", idsQueryBuilder.queryName()); + assertEquals(1.5f, idsQueryBuilder.boost(), 0.001f); + assertEquals(2, idsQueryBuilder.ids().size()); + assertTrue(idsQueryBuilder.ids().contains("doc1")); + assertTrue(idsQueryBuilder.ids().contains("doc2")); + } + + public void testFromProtoWithMinimalIdsQuery() { + // Create a minimal IdsQuery + IdsQuery idsQuery = IdsQuery.newBuilder().build(); + + // Create QueryContainer with IdsQuery + QueryContainer queryContainer = QueryContainer.newBuilder().setIds(idsQuery).build(); + + // Call the method under test + QueryBuilder result = converter.fromProto(queryContainer); + + // Verify the result + assertNotNull(result); + assertTrue(result instanceof IdsQueryBuilder); + + IdsQueryBuilder idsQueryBuilder = (IdsQueryBuilder) result; + assertNull(idsQueryBuilder.queryName()); + assertEquals(1.0f, idsQueryBuilder.boost(), 0.001f); + assertEquals(0, idsQueryBuilder.ids().size()); + } + + public void testFromProtoWithNullQueryContainer() { + // Test with null QueryContainer + expectThrows(IllegalArgumentException.class, () -> { converter.fromProto(null); }); + } + + public void testFromProtoWithoutIdsQuery() { + // Create QueryContainer without IdsQuery (empty) + QueryContainer queryContainer = QueryContainer.newBuilder().build(); + + // Test should throw IllegalArgumentException + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { converter.fromProto(queryContainer); }); + + assertTrue(exception.getMessage().contains("QueryContainer does not contain an Ids query")); + } + + public void testFromProtoWithDifferentQueryType() { + // Create QueryContainer with a different query type (not IdsQuery) + QueryContainer queryContainer = QueryContainer.newBuilder() + .setBool(org.opensearch.protobufs.BoolQuery.newBuilder().build()) + .build(); + + // Test should throw IllegalArgumentException + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { converter.fromProto(queryContainer); }); + + assertTrue(exception.getMessage().contains("QueryContainer does not contain an Ids query")); + } + + public void testFromProtoWithComplexIdsQuery() { + // Create a complex IdsQuery with many values + IdsQuery.Builder idsProtoBuilder = IdsQuery.newBuilder().setXName("complex_ids_query").setBoost(2.0f); + + // Add multiple values + for (int i = 0; i < 10; i++) { + idsProtoBuilder.addValues("doc_" + i); + } + + IdsQuery idsQuery = idsProtoBuilder.build(); + + // Create QueryContainer with IdsQuery + QueryContainer queryContainer = QueryContainer.newBuilder().setIds(idsQuery).build(); + + // Call the method under test + QueryBuilder result = converter.fromProto(queryContainer); + + // Verify the result + assertNotNull(result); + assertTrue(result instanceof IdsQueryBuilder); + + IdsQueryBuilder idsQueryBuilder = (IdsQueryBuilder) result; + assertEquals("complex_ids_query", idsQueryBuilder.queryName()); + assertEquals(2.0f, idsQueryBuilder.boost(), 0.001f); + assertEquals(10, idsQueryBuilder.ids().size()); + + // Verify all IDs are present + for (int i = 0; i < 10; i++) { + assertTrue(idsQueryBuilder.ids().contains("doc_" + i)); + } + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoUtilsTests.java new file mode 100644 index 0000000000000..71531ba19905c --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/IdsQueryBuilderProtoUtilsTests.java @@ -0,0 +1,175 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.IdsQueryBuilder; +import org.opensearch.protobufs.IdsQuery; +import org.opensearch.test.OpenSearchTestCase; + +import java.util.Set; + +import static org.opensearch.transport.grpc.proto.request.search.query.IdsQueryBuilderProtoUtils.fromProto; + +public class IdsQueryBuilderProtoUtilsTests extends OpenSearchTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + // Set up the registry with all built-in converters + QueryBuilderProtoTestUtils.setupRegistry(); + } + + public void testFromProtoWithAllFields() { + // Create a protobuf IdsQuery with all fields + IdsQuery idsQuery = IdsQuery.newBuilder() + .setXName("test_ids_query") + .setBoost(1.5f) + .addValues("doc1") + .addValues("doc2") + .addValues("doc3") + .build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + assertEquals("test_ids_query", result.queryName()); + assertEquals(1.5f, result.boost(), 0.001f); + + Set ids = result.ids(); + assertEquals(3, ids.size()); + assertTrue(ids.contains("doc1")); + assertTrue(ids.contains("doc2")); + assertTrue(ids.contains("doc3")); + } + + public void testFromProtoWithMinimalFields() { + // Create a protobuf IdsQuery with only required fields + IdsQuery idsQuery = IdsQuery.newBuilder().build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + assertNull(result.queryName()); + assertEquals(1.0f, result.boost(), 0.001f); + assertEquals(0, result.ids().size()); + } + + public void testFromProtoWithBoostOnly() { + // Create a protobuf IdsQuery with only boost set + IdsQuery idsQuery = IdsQuery.newBuilder().setBoost(2.5f).build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + assertNull(result.queryName()); + assertEquals(2.5f, result.boost(), 0.001f); + assertEquals(0, result.ids().size()); + } + + public void testFromProtoWithNameOnly() { + // Create a protobuf IdsQuery with only name set + IdsQuery idsQuery = IdsQuery.newBuilder().setXName("my_ids_query").build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + assertEquals("my_ids_query", result.queryName()); + assertEquals(1.0f, result.boost(), 0.001f); + assertEquals(0, result.ids().size()); + } + + public void testFromProtoWithValuesOnly() { + // Create a protobuf IdsQuery with only values set + IdsQuery idsQuery = IdsQuery.newBuilder().addValues("id1").addValues("id2").build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + assertNull(result.queryName()); + assertEquals(1.0f, result.boost(), 0.001f); + + Set ids = result.ids(); + assertEquals(2, ids.size()); + assertTrue(ids.contains("id1")); + assertTrue(ids.contains("id2")); + } + + public void testFromProtoWithSingleValue() { + // Create a protobuf IdsQuery with a single value + IdsQuery idsQuery = IdsQuery.newBuilder().addValues("single_doc_id").build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + Set ids = result.ids(); + assertEquals(1, ids.size()); + assertTrue(ids.contains("single_doc_id")); + } + + public void testFromProtoWithDuplicateValues() { + // Create a protobuf IdsQuery with duplicate values + IdsQuery idsQuery = IdsQuery.newBuilder() + .addValues("doc1") + .addValues("doc2") + .addValues("doc1") // duplicate + .build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result - IdsQueryBuilder uses a Set internally, so duplicates should be removed + Set ids = result.ids(); + assertEquals(2, ids.size()); // Should only have 2 unique values + assertTrue(ids.contains("doc1")); + assertTrue(ids.contains("doc2")); + } + + public void testFromProtoWithEmptyStringValue() { + // Create a protobuf IdsQuery with empty string value + IdsQuery idsQuery = IdsQuery.newBuilder().addValues("").addValues("doc1").build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + Set ids = result.ids(); + assertEquals(2, ids.size()); + assertTrue(ids.contains("")); + assertTrue(ids.contains("doc1")); + } + + public void testFromProtoWithZeroBoost() { + // Create a protobuf IdsQuery with zero boost + IdsQuery idsQuery = IdsQuery.newBuilder().setBoost(0.0f).addValues("doc1").build(); + + // Call the method under test + IdsQueryBuilder result = fromProto(idsQuery); + + // Verify the result + assertEquals(0.0f, result.boost(), 0.001f); + assertEquals(1, result.ids().size()); + } + + public void testFromProtoWithNegativeBoost() { + // Create a protobuf IdsQuery with negative boost + IdsQuery idsQuery = IdsQuery.newBuilder().setBoost(-1.0f).addValues("doc1").build(); + + // Call the method under test - should throw IllegalArgumentException for negative boost + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { fromProto(idsQuery); }); + + // Verify the exception message + assertTrue("Exception message should mention negative boost", exception.getMessage().contains("negative [boost] are not allowed")); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java index b4d685d560f81..d717d6e226cb6 100644 --- a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java @@ -18,6 +18,7 @@ import org.opensearch.protobufs.GeoBounds; import org.opensearch.protobufs.GeoDistanceQuery; import org.opensearch.protobufs.GeoLocation; +import org.opensearch.protobufs.IdsQuery; import org.opensearch.protobufs.InlineScript; import org.opensearch.protobufs.LatLonGeoLocation; import org.opensearch.protobufs.MatchAllQuery; @@ -25,11 +26,16 @@ import org.opensearch.protobufs.MinimumShouldMatch; import org.opensearch.protobufs.MultiMatchQuery; import org.opensearch.protobufs.NestedQuery; +import org.opensearch.protobufs.NumberRangeQuery; +import org.opensearch.protobufs.NumberRangeQueryAllOfFrom; +import org.opensearch.protobufs.NumberRangeQueryAllOfTo; import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.RangeQuery; import org.opensearch.protobufs.RegexpQuery; import org.opensearch.protobufs.Script; import org.opensearch.protobufs.ScriptQuery; import org.opensearch.protobufs.TermQuery; +import org.opensearch.protobufs.TermsSetQuery; import org.opensearch.protobufs.TextQueryType; import org.opensearch.protobufs.WildcardQuery; import org.opensearch.test.OpenSearchTestCase; @@ -197,70 +203,51 @@ public QueryBuilder fromProto(QueryContainer queryContainer) { expectThrows(IllegalArgumentException.class, () -> registry.registerConverter(customConverter)); } - public void testGeoDistanceQueryConversion() { - // Create a GeoDistance query container - LatLonGeoLocation latLonLocation = LatLonGeoLocation.newBuilder().setLat(40.7589).setLon(-73.9851).build(); - - GeoLocation geoLocation = GeoLocation.newBuilder().setLatlon(latLonLocation).build(); - - GeoDistanceQuery geoDistanceQuery = GeoDistanceQuery.newBuilder() - .setXName("location") - .setDistance("10km") - .putLocation("location", geoLocation) - .build(); + public void testIdsQueryConversion() { + // Create an Ids query container + IdsQuery idsQuery = IdsQuery.newBuilder().addValues("doc1").addValues("doc2").setBoost(1.5f).setXName("test_ids_query").build(); - QueryContainer queryContainer = QueryContainer.newBuilder().setGeoDistance(geoDistanceQuery).build(); + QueryContainer queryContainer = QueryContainer.newBuilder().setIds(idsQuery).build(); // Convert using the registry QueryBuilder queryBuilder = registry.fromProto(queryContainer); // Verify the result assertNotNull("QueryBuilder should not be null", queryBuilder); - assertEquals( - "Should be a GeoDistanceQueryBuilder", - "org.opensearch.index.query.GeoDistanceQueryBuilder", - queryBuilder.getClass().getName() - ); + assertEquals("Should be an IdsQueryBuilder", "org.opensearch.index.query.IdsQueryBuilder", queryBuilder.getClass().getName()); } - public void testGeoBoundingBoxQueryConversion() { - // Create a GeoBoundingBox query container - CoordsGeoBounds coords = CoordsGeoBounds.newBuilder().setTop(40.7).setLeft(-74.0).setBottom(40.6).setRight(-73.9).build(); + public void testRangeQueryConversion() { + // Create a Range query container with NumberRangeQuery + NumberRangeQueryAllOfFrom fromValue = NumberRangeQueryAllOfFrom.newBuilder().setDouble(10.0).build(); + NumberRangeQueryAllOfTo toValue = NumberRangeQueryAllOfTo.newBuilder().setDouble(100.0).build(); - GeoBounds geoBounds = GeoBounds.newBuilder().setCoords(coords).build(); + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder().setField("age").setFrom(fromValue).setTo(toValue).build(); - GeoBoundingBoxQuery geoBoundingBoxQuery = GeoBoundingBoxQuery.newBuilder().putBoundingBox("location", geoBounds).build(); + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); - QueryContainer queryContainer = QueryContainer.newBuilder().setGeoBoundingBox(geoBoundingBoxQuery).build(); + QueryContainer queryContainer = QueryContainer.newBuilder().setRange(rangeQuery).build(); // Convert using the registry QueryBuilder queryBuilder = registry.fromProto(queryContainer); // Verify the result assertNotNull("QueryBuilder should not be null", queryBuilder); - assertEquals( - "Should be a GeoBoundingBoxQueryBuilder", - "org.opensearch.index.query.GeoBoundingBoxQueryBuilder", - queryBuilder.getClass().getName() - ); + assertEquals("Should be a RangeQueryBuilder", "org.opensearch.index.query.RangeQueryBuilder", queryBuilder.getClass().getName()); } - public void testGeoDistanceQueryConversionWithDoubleArray() { - // Create a GeoDistance query with DoubleArray format - DoubleArray doubleArray = DoubleArray.newBuilder() - .addDoubleArray(-73.9851) // lon - .addDoubleArray(40.7589) // lat + public void testTermsSetQueryConversion() { + // Create a TermsSet query container + TermsSetQuery termsSetQuery = TermsSetQuery.newBuilder() + .setField("tags") + .addTerms("urgent") + .addTerms("important") + .setMinimumShouldMatchField("tag_count") + .setBoost(2.0f) + .setXName("test_terms_set_query") .build(); - GeoLocation geoLocation = GeoLocation.newBuilder().setDoubleArray(doubleArray).build(); - - GeoDistanceQuery geoDistanceQuery = GeoDistanceQuery.newBuilder() - .setXName("location") - .setDistance("5mi") - .putLocation("location", geoLocation) - .build(); - - QueryContainer queryContainer = QueryContainer.newBuilder().setGeoDistance(geoDistanceQuery).build(); + QueryContainer queryContainer = QueryContainer.newBuilder().setTermsSet(termsSetQuery).build(); // Convert using the registry QueryBuilder queryBuilder = registry.fromProto(queryContainer); @@ -268,8 +255,8 @@ public void testGeoDistanceQueryConversionWithDoubleArray() { // Verify the result assertNotNull("QueryBuilder should not be null", queryBuilder); assertEquals( - "Should be a GeoDistanceQueryBuilder", - "org.opensearch.index.query.GeoDistanceQueryBuilder", + "Should be a TermsSetQueryBuilder", + "org.opensearch.index.query.TermsSetQueryBuilder", queryBuilder.getClass().getName() ); } @@ -404,6 +391,83 @@ public void testBoolQueryConversion() { assertEquals("Should be a BoolQueryBuilder", "org.opensearch.index.query.BoolQueryBuilder", queryBuilder.getClass().getName()); } + public void testGeoDistanceQueryConversion() { + // Create a GeoDistance query container + LatLonGeoLocation latLonLocation = LatLonGeoLocation.newBuilder().setLat(40.7589).setLon(-73.9851).build(); + + GeoLocation geoLocation = GeoLocation.newBuilder().setLatlon(latLonLocation).build(); + + GeoDistanceQuery geoDistanceQuery = GeoDistanceQuery.newBuilder() + .setXName("location") + .setDistance("10km") + .putLocation("location", geoLocation) + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setGeoDistance(geoDistanceQuery).build(); + + // Convert using the registry + QueryBuilder queryBuilder = registry.fromProto(queryContainer); + + // Verify the result + assertNotNull("QueryBuilder should not be null", queryBuilder); + assertEquals( + "Should be a GeoDistanceQueryBuilder", + "org.opensearch.index.query.GeoDistanceQueryBuilder", + queryBuilder.getClass().getName() + ); + } + + public void testGeoBoundingBoxQueryConversion() { + // Create a GeoBoundingBox query container + CoordsGeoBounds coords = CoordsGeoBounds.newBuilder().setTop(40.7).setLeft(-74.0).setBottom(40.6).setRight(-73.9).build(); + + GeoBounds geoBounds = GeoBounds.newBuilder().setCoords(coords).build(); + + GeoBoundingBoxQuery geoBoundingBoxQuery = GeoBoundingBoxQuery.newBuilder().putBoundingBox("location", geoBounds).build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setGeoBoundingBox(geoBoundingBoxQuery).build(); + + // Convert using the registry + QueryBuilder queryBuilder = registry.fromProto(queryContainer); + + // Verify the result + assertNotNull("QueryBuilder should not be null", queryBuilder); + assertEquals( + "Should be a GeoBoundingBoxQueryBuilder", + "org.opensearch.index.query.GeoBoundingBoxQueryBuilder", + queryBuilder.getClass().getName() + ); + } + + public void testGeoDistanceQueryConversionWithDoubleArray() { + // Create a GeoDistance query with DoubleArray format + DoubleArray doubleArray = DoubleArray.newBuilder() + .addDoubleArray(-73.9851) // lon + .addDoubleArray(40.7589) // lat + .build(); + + GeoLocation geoLocation = GeoLocation.newBuilder().setDoubleArray(doubleArray).build(); + + GeoDistanceQuery geoDistanceQuery = GeoDistanceQuery.newBuilder() + .setXName("location") + .setDistance("5mi") + .putLocation("location", geoLocation) + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setGeoDistance(geoDistanceQuery).build(); + + // Convert using the registry + QueryBuilder queryBuilder = registry.fromProto(queryContainer); + + // Verify the result + assertNotNull("QueryBuilder should not be null", queryBuilder); + assertEquals( + "Should be a GeoDistanceQueryBuilder", + "org.opensearch.index.query.GeoDistanceQueryBuilder", + queryBuilder.getClass().getName() + ); + } + public void testRegisterConverter() { // Create a custom converter for testing QueryBuilderProtoConverter customConverter = new QueryBuilderProtoConverter() { diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoConverterTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoConverterTests.java new file mode 100644 index 0000000000000..e7c0cbf66ecec --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoConverterTests.java @@ -0,0 +1,84 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.RangeQueryBuilder; +import org.opensearch.protobufs.DateRangeQuery; +import org.opensearch.protobufs.DateRangeQueryAllOfFrom; +import org.opensearch.protobufs.DateRangeQueryAllOfTo; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.RangeQuery; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class RangeQueryBuilderProtoConverterTests extends OpenSearchTestCase { + + private RangeQueryBuilderProtoConverter converter; + + @Override + public void setUp() throws Exception { + super.setUp(); + converter = new RangeQueryBuilderProtoConverter(); + } + + public void testGetHandledQueryCase() { + assertEquals(QueryContainer.QueryContainerCase.RANGE, converter.getHandledQueryCase()); + } + + public void testFromProtoWithValidRangeQuery() { + DateRangeQueryAllOfFrom fromObj = DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build(); + + DateRangeQueryAllOfTo toObj = DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build(); + + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder().setField("date_field").setFrom(fromObj).setTo(toObj).build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setRange(rangeQuery).build(); + + QueryBuilder result = converter.fromProto(queryContainer); + + assertThat(result, instanceOf(RangeQueryBuilder.class)); + RangeQueryBuilder rangeQueryBuilder = (RangeQueryBuilder) result; + assertEquals("date_field", rangeQueryBuilder.fieldName()); + } + + public void testFromProtoWithNullQueryContainer() { + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(null)); + + assertThat(exception.getMessage(), containsString("QueryContainer must contain a RangeQuery")); + } + + public void testFromProtoWithWrongQueryType() { + QueryContainer queryContainer = QueryContainer.newBuilder() + .setTerm( + org.opensearch.protobufs.TermQuery.newBuilder() + .setField("test_field") + .setValue(org.opensearch.protobufs.FieldValue.newBuilder().setString("test_value").build()) + .build() + ) + .build(); + + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(queryContainer)); + + assertThat(exception.getMessage(), containsString("QueryContainer must contain a RangeQuery")); + } + + public void testFromProtoWithUnsetQueryContainer() { + + QueryContainer queryContainer = QueryContainer.newBuilder().build(); + + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(queryContainer)); + + assertThat(exception.getMessage(), containsString("QueryContainer must contain a RangeQuery")); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoUtilsTests.java new file mode 100644 index 0000000000000..d88724a4fabea --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/RangeQueryBuilderProtoUtilsTests.java @@ -0,0 +1,458 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.RangeQueryBuilder; +import org.opensearch.protobufs.DateRangeQuery; +import org.opensearch.protobufs.DateRangeQueryAllOfFrom; +import org.opensearch.protobufs.DateRangeQueryAllOfTo; +import org.opensearch.protobufs.NullValue; +import org.opensearch.protobufs.NumberRangeQuery; +import org.opensearch.protobufs.NumberRangeQueryAllOfFrom; +import org.opensearch.protobufs.NumberRangeQueryAllOfTo; +import org.opensearch.protobufs.RangeQuery; +import org.opensearch.protobufs.RangeRelation; +import org.opensearch.test.OpenSearchTestCase; + +public class RangeQueryBuilderProtoUtilsTests extends OpenSearchTestCase { + + // ========== DateRangeQuery Tests ========== + + public void testFromProtoWithDateRangeQuery() { + // Create a protobuf DateRangeQuery with all parameters + DateRangeQueryAllOfFrom fromObj = DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build(); + + DateRangeQueryAllOfTo toObj = DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build(); + + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder().setField("date_field").setFrom(fromObj).setTo(toObj).build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + + RangeQueryBuilder rangeQueryBuilder = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", rangeQueryBuilder); + assertEquals("Field name should match", "date_field", rangeQueryBuilder.fieldName()); + assertEquals("From should match", "2023-01-01", rangeQueryBuilder.from()); + assertEquals("To should match", "2023-12-31", rangeQueryBuilder.to()); + } + + public void testFromProtoWithDateRangeQueryNullFromTo() { + // Test DateRangeQuery with null enum values in oneof from/to fields + DateRangeQueryAllOfFrom fromObj = DateRangeQueryAllOfFrom.newBuilder().setNullValue(NullValue.NULL_VALUE_NULL).build(); + + DateRangeQueryAllOfTo toObj = DateRangeQueryAllOfTo.newBuilder().setNullValue(NullValue.NULL_VALUE_NULL).build(); + + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder().setField("date_field").setFrom(fromObj).setTo(toObj).build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + + RangeQueryBuilder rangeQueryBuilder = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", rangeQueryBuilder); + assertEquals("Field name should match", "date_field", rangeQueryBuilder.fieldName()); + assertNull("From should be null (unbounded)", rangeQueryBuilder.from()); + assertNull("To should be null (unbounded)", rangeQueryBuilder.to()); + } + + // ========== NumberRangeQuery Tests ========== + + public void testFromProtoWithNumberRangeQuery() { + // Test NumberRangeQuery with double values in oneof from/to fields + NumberRangeQueryAllOfFrom fromObj = NumberRangeQueryAllOfFrom.newBuilder().setDouble(10.0).build(); + + NumberRangeQueryAllOfTo toObj = NumberRangeQueryAllOfTo.newBuilder().setDouble(100.0).build(); + + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder().setField("number_field").setFrom(fromObj).setTo(toObj).build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + + RangeQueryBuilder rangeQueryBuilder = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", rangeQueryBuilder); + assertEquals("Field name should match", "number_field", rangeQueryBuilder.fieldName()); + assertEquals("From should match", 10.0, rangeQueryBuilder.from()); + assertEquals("To should match", 100.0, rangeQueryBuilder.to()); + } + + // ========== Precedence Tests (gt/gte/lt/lte override from/to) ========== + + public void testGteLteOverridesFromToAndIncludeFlags() { + // Test that gte/lte fields override from/to and include_lower/include_upper + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("date_field") + .setGte("2023-06-01") // Should override from and set includeLower=true + .setLte("2023-12-31") // Should set to and set includeUpper=true + .setIncludeLower(false) // Should be overridden by gte + .setIncludeUpper(false) // Should be overridden by lte + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + + RangeQueryBuilder rangeQueryBuilder = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", rangeQueryBuilder); + assertEquals("Field name should match", "date_field", rangeQueryBuilder.fieldName()); + assertEquals("From should be from gte", "2023-06-01", rangeQueryBuilder.from()); + assertEquals("To should be from lte", "2023-12-31", rangeQueryBuilder.to()); + assertTrue("includeLower should be true (from gte)", rangeQueryBuilder.includeLower()); + assertTrue("includeUpper should be true (from lte)", rangeQueryBuilder.includeUpper()); + } + + // ========== Error Cases ========== + + public void testFromProtoWithNullRangeQuery() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> RangeQueryBuilderProtoUtils.fromProto(null) + ); + assertEquals("RangeQuery cannot be null", exception.getMessage()); + } + + public void testFromProtoWithEmptyRangeQuery() { + RangeQuery rangeQuery = RangeQuery.newBuilder().build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> RangeQueryBuilderProtoUtils.fromProto(rangeQuery) + ); + assertEquals("RangeQuery must contain either DateRangeQuery or NumberRangeQuery", exception.getMessage()); + } + + // ========== Additional DateRangeQuery Coverage Tests ========== + + public void testFromProtoWithDateRangeQueryAllOptionalFields() { + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("timestamp") + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01T00:00:00Z").build()) + .setTo(DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31T23:59:59Z").build()) + .setGt("2023-01-02") + .setGte("2023-01-01") + .setLt("2023-12-31") + .setLte("2023-12-30") + .setIncludeLower(true) + .setIncludeUpper(false) + .setFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + .setTimeZone("UTC") + .setBoost(2.5f) + .setXName("comprehensive_date_range") + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "timestamp", result.fieldName()); + assertEquals("Query name should match", "comprehensive_date_range", result.queryName()); + assertEquals("Boost should match", 2.5f, result.boost(), 0.001f); + assertEquals("Format should match", "yyyy-MM-dd'T'HH:mm:ss'Z'", result.format()); + assertEquals("Time zone should match", "UTC", result.timeZone()); + } + + public void testFromProtoWithDateRangeQueryEmptyField() { + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("") // Empty field name + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build()) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> RangeQueryBuilderProtoUtils.fromProto(rangeQuery) + ); + assertTrue( + "Exception should mention field name requirement", + exception.getMessage().contains("Field name cannot be null or empty") + ); + } + + public void testFromProtoWithDateRangeQueryGtGteOverrides() { + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("date_field") + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build()) // Should be overridden + .setGt("2023-02-01") // Should override 'from' + .setGte("2023-03-01") // Should override 'gt' + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("From should be from gte (final precedence)", "2023-03-01", result.from()); + assertTrue("Should include lower when using gte", result.includeLower()); + } + + public void testFromProtoWithDateRangeQueryLtLteOverrides() { + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("date_field") + .setTo(DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build()) // Should be overridden + .setLt("2023-11-30") // Should override 'to' + .setLte("2023-10-30") // Should override 'lt' + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("To should be from lte (final precedence)", "2023-10-30", result.to()); + assertTrue("Should include upper when using lte", result.includeUpper()); + } + + public void testFromProtoWithDateRangeQueryWithRelation() { + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("date_field") + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build()) + .setTo(DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build()) + .setRelation(RangeRelation.RANGE_RELATION_INTERSECTS) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "date_field", result.fieldName()); + // Note: The relation should be set but we can't easily test it without accessing private fields + } + + public void testFromProtoWithDateRangeQueryMinimalFields() { + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder().setField("minimal_date_field").build(); // Only field name set + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "minimal_date_field", result.fieldName()); + assertEquals("Default boost should be applied", 1.0f, result.boost(), 0.001f); + assertNull("Query name should be null", result.queryName()); + } + + // ========== Additional NumberRangeQuery Coverage Tests ========== + + public void testFromProtoWithNumberRangeQueryAllOptionalFields() { + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("score") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(10.5).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(100.0).build()) + .setGt(5.0) + .setGte(10.0) + .setLt(90.0) + .setLte(85.0) + .setIncludeLower(false) + .setIncludeUpper(true) + .setBoost(1.8f) + .setXName("comprehensive_number_range") + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "score", result.fieldName()); + assertEquals("Query name should match", "comprehensive_number_range", result.queryName()); + assertEquals("Boost should match", 1.8f, result.boost(), 0.001f); + } + + public void testFromProtoWithNumberRangeQueryEmptyField() { + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("") // Empty field name + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(10.0).build()) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> RangeQueryBuilderProtoUtils.fromProto(rangeQuery) + ); + assertTrue( + "Exception should mention field name requirement", + exception.getMessage().contains("Field name cannot be null or empty") + ); + } + + public void testFromProtoWithNumberRangeQueryNullValues() { + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("null_range_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setNullValue(NullValue.NULL_VALUE_NULL).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setNullValue(NullValue.NULL_VALUE_NULL).build()) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "null_range_field", result.fieldName()); + assertNull("From should be null", result.from()); + assertNull("To should be null", result.to()); + } + + public void testFromProtoWithNumberRangeQueryDoubleValues() { + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("double_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(50.0).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(150.0).build()) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "double_field", result.fieldName()); + assertEquals("From should be double", 50.0, result.from()); + assertEquals("To should be double", 150.0, result.to()); + } + + public void testFromProtoWithNumberRangeQueryWithRelation() { + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("number_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(1.0).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(10.0).build()) + .setRelation(RangeRelation.RANGE_RELATION_CONTAINS) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "number_field", result.fieldName()); + } + + // ========== Missing Coverage Tests ========== + + public void testFromProtoWithNumberRangeQueryStringValues() { + // Test NumberRangeQuery with string values to cover hasString() branches + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("string_number_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setString("10.5").build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setString("100.5").build()) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "string_number_field", result.fieldName()); + assertEquals("From should be string", "10.5", result.from()); + assertEquals("To should be string", "100.5", result.to()); + } + + public void testFromProtoWithNumberRangeQueryMixedStringDouble() { + // Test NumberRangeQuery with mixed string and double values + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("mixed_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setString("5.0").build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(50.0).build()) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "mixed_field", result.fieldName()); + assertEquals("From should be string", "5.0", result.from()); + assertEquals("To should be double", 50.0, result.to()); + } + + public void testFromProtoWithDateRangeQueryWithinRelation() { + // Test DateRangeQuery with RANGE_RELATION_WITHIN to cover missing parseRangeRelation case + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("date_field") + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build()) + .setTo(DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build()) + .setRelation(RangeRelation.RANGE_RELATION_WITHIN) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "date_field", result.fieldName()); + } + + public void testFromProtoWithNumberRangeQueryWithinRelation() { + // Test NumberRangeQuery with RANGE_RELATION_WITHIN + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("within_number_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(1.0).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(10.0).build()) + .setRelation(RangeRelation.RANGE_RELATION_WITHIN) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "within_number_field", result.fieldName()); + } + + public void testFromProtoWithDateRangeQueryUnspecifiedRelation() { + // Test DateRangeQuery with RANGE_RELATION_UNSPECIFIED to trigger default case in parseRangeRelation + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("unspecified_relation_field") + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build()) + .setTo(DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build()) + .setRelation(RangeRelation.RANGE_RELATION_UNSPECIFIED) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "unspecified_relation_field", result.fieldName()); + } + + public void testFromProtoWithNumberRangeQueryUnspecifiedRelation() { + // Test NumberRangeQuery with RANGE_RELATION_UNSPECIFIED to trigger default case + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("unspecified_number_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(1.0).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(10.0).build()) + .setRelation(RangeRelation.RANGE_RELATION_UNSPECIFIED) + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "unspecified_number_field", result.fieldName()); + } + + public void testFromProtoWithDateRangeQueryNoRelation() { + // Test DateRangeQuery without relation field set to cover null relation case + DateRangeQuery dateRangeQuery = DateRangeQuery.newBuilder() + .setField("no_relation_field") + .setFrom(DateRangeQueryAllOfFrom.newBuilder().setString("2023-01-01").build()) + .setTo(DateRangeQueryAllOfTo.newBuilder().setString("2023-12-31").build()) + // Note: No .setRelation() call - relation will be null/unset + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setDateRangeQuery(dateRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "no_relation_field", result.fieldName()); + } + + public void testFromProtoWithNumberRangeQueryNoRelation() { + // Test NumberRangeQuery without relation field set to cover null relation case + NumberRangeQuery numberRangeQuery = NumberRangeQuery.newBuilder() + .setField("no_relation_number_field") + .setFrom(NumberRangeQueryAllOfFrom.newBuilder().setDouble(1.0).build()) + .setTo(NumberRangeQueryAllOfTo.newBuilder().setDouble(10.0).build()) + // Note: No .setRelation() call - relation will be null/unset + .build(); + + RangeQuery rangeQuery = RangeQuery.newBuilder().setNumberRangeQuery(numberRangeQuery).build(); + RangeQueryBuilder result = RangeQueryBuilderProtoUtils.fromProto(rangeQuery); + + assertNotNull("RangeQueryBuilder should not be null", result); + assertEquals("Field name should match", "no_relation_number_field", result.fieldName()); + } + +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoConverterTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoConverterTests.java new file mode 100644 index 0000000000000..8ebfb0e30ddae --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoConverterTests.java @@ -0,0 +1,74 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.TermsSetQueryBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.TermsSetQuery; +import org.opensearch.test.OpenSearchTestCase; + +public class TermsSetQueryBuilderProtoConverterTests extends OpenSearchTestCase { + + private TermsSetQueryBuilderProtoConverter converter; + + @Override + public void setUp() throws Exception { + super.setUp(); + converter = new TermsSetQueryBuilderProtoConverter(); + } + + public void testGetHandledQueryCase() { + assertEquals(QueryContainer.QueryContainerCase.TERMS_SET, converter.getHandledQueryCase()); + } + + public void testFromProtoValid() { + TermsSetQuery termsSetQuery = TermsSetQuery.newBuilder() + .setField("status") + .addTerms("published") + .setMinimumShouldMatchField("count") + .setBoost(1.5f) + .setXName("status_query") + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setTermsSet(termsSetQuery).build(); + + QueryBuilder result = converter.fromProto(queryContainer); + + assertNotNull(result); + assertTrue(result instanceof TermsSetQueryBuilder); + + TermsSetQueryBuilder termsSetResult = (TermsSetQueryBuilder) result; + assertEquals(1, termsSetResult.getValues().size()); + Object value = termsSetResult.getValues().get(0); + String stringValue = value instanceof org.apache.lucene.util.BytesRef + ? ((org.apache.lucene.util.BytesRef) value).utf8ToString() + : value.toString(); + assertEquals("published", stringValue); + assertEquals(1.5f, termsSetResult.boost(), 0.0f); + assertEquals("status_query", termsSetResult.queryName()); + } + + public void testFromProtoWithNullInput() { + expectThrows(IllegalArgumentException.class, () -> converter.fromProto(null)); + } + + public void testFromProtoWithoutTermsSet() { + QueryContainer queryContainer = QueryContainer.newBuilder().build(); + expectThrows(IllegalArgumentException.class, () -> converter.fromProto(queryContainer)); + } + + public void testFromProtoWithDifferentQueryType() { + QueryContainer queryContainer = QueryContainer.newBuilder() + .setMatchAll(org.opensearch.protobufs.MatchAllQuery.newBuilder().build()) + .build(); + + expectThrows(IllegalArgumentException.class, () -> converter.fromProto(queryContainer)); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoUtilsTests.java new file mode 100644 index 0000000000000..693ccc4dd11ea --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsSetQueryBuilderProtoUtilsTests.java @@ -0,0 +1,232 @@ +/* + * 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.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.TermsSetQueryBuilder; +import org.opensearch.protobufs.BuiltinScriptLanguage; +import org.opensearch.protobufs.InlineScript; +import org.opensearch.protobufs.Script; +import org.opensearch.protobufs.ScriptLanguage; +import org.opensearch.protobufs.TermsSetQuery; +import org.opensearch.test.OpenSearchTestCase; + +public class TermsSetQueryBuilderProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoBasic() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("status") + .addTerms("published") + .setMinimumShouldMatchField("count") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(1, result.getValues().size()); + Object value = result.getValues().get(0); + String stringValue = value instanceof org.apache.lucene.util.BytesRef + ? ((org.apache.lucene.util.BytesRef) value).utf8ToString() + : value.toString(); + assertEquals("published", stringValue); + assertEquals(AbstractQueryBuilder.DEFAULT_BOOST, result.boost(), 0.0f); + assertNull(result.queryName()); + } + + public void testFromProtoWithOptionalFields() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("tags") + .addTerms("urgent") + .setBoost(2.0f) + .setXName("test_query") + .setMinimumShouldMatchField("tag_count") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(2.0f, result.boost(), 0.0f); + assertEquals("test_query", result.queryName()); + assertEquals("tag_count", result.getMinimumShouldMatchField()); + } + + public void testFromProtoWithScript() { + Script script = Script.newBuilder() + .setInline( + InlineScript.newBuilder() + .setSource("params.num_terms") + .setLang(ScriptLanguage.newBuilder().setBuiltin(BuiltinScriptLanguage.BUILTIN_SCRIPT_LANGUAGE_PAINLESS)) + .build() + ) + .build(); + + TermsSetQuery proto = TermsSetQuery.newBuilder().setField("skills").addTerms("java").setMinimumShouldMatchScript(script).build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertNull(result.getMinimumShouldMatchField()); + assertNotNull(result.getMinimumShouldMatchScript()); + assertEquals("painless", result.getMinimumShouldMatchScript().getLang()); + } + + public void testFromProtoWithNullInput() { + expectThrows(IllegalArgumentException.class, () -> TermsSetQueryBuilderProtoUtils.fromProto(null)); + } + + public void testFromProtoWithEmptyField() { + TermsSetQuery proto = TermsSetQuery.newBuilder().setField("").addTerms("value").setMinimumShouldMatchField("count").build(); + + expectThrows(IllegalArgumentException.class, () -> TermsSetQueryBuilderProtoUtils.fromProto(proto)); + } + + public void testFromProtoWithNoTerms() { + TermsSetQuery proto = TermsSetQuery.newBuilder().setField("category").setMinimumShouldMatchField("count").build(); + + expectThrows(IllegalArgumentException.class, () -> TermsSetQueryBuilderProtoUtils.fromProto(proto)); + } + + public void testFromProtoWithNullField() { + // No field set should default to null or empty + TermsSetQuery proto = TermsSetQuery.newBuilder().addTerms("value").setMinimumShouldMatchField("count").build(); + + expectThrows(IllegalArgumentException.class, () -> TermsSetQueryBuilderProtoUtils.fromProto(proto)); + } + + public void testFromProtoWithMultipleTerms() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("categories") + .addTerms("tech") + .addTerms("science") + .addTerms("programming") + .addTerms("java") + .setMinimumShouldMatchField("category_count") + .setBoost(1.5f) + .setXName("multi_terms_query") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(4, result.getValues().size()); + // Note: Field name is internal to the builder and not directly accessible for verification + assertEquals("category_count", result.getMinimumShouldMatchField()); + assertEquals(1.5f, result.boost(), 0.0f); + assertEquals("multi_terms_query", result.queryName()); + } + + public void testFromProtoMinimalFieldsOnly() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("minimal_field") + .addTerms("single_term") + .setMinimumShouldMatchField("min_count") + .build(); // No boost or query name - should use defaults + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(1, result.getValues().size()); + assertEquals("min_count", result.getMinimumShouldMatchField()); + assertEquals(AbstractQueryBuilder.DEFAULT_BOOST, result.boost(), 0.0f); + assertNull(result.queryName()); + } + + public void testFromProtoWithEmptyTermsString() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("test_field") + .addTerms("") // Empty string term + .addTerms("valid_term") + .setMinimumShouldMatchField("count") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(2, result.getValues().size()); + // Both empty string and valid term should be included + } + + public void testFromProtoWithDuplicateTerms() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("duplicate_field") + .addTerms("term1") + .addTerms("term2") + .addTerms("term1") // Duplicate + .addTerms("term2") // Duplicate + .setMinimumShouldMatchField("term_count") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(4, result.getValues().size()); // All terms included, including duplicates + } + + public void testFromProtoWithZeroBoost() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("boost_field") + .addTerms("term") + .setBoost(0.0f) // Zero boost + .setMinimumShouldMatchField("count") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals(0.0f, result.boost(), 0.0f); + } + + public void testFromProtoWithNegativeBoost() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("negative_boost_field") + .addTerms("term") + .setBoost(-1.0f) // Negative boost - should throw exception + .setMinimumShouldMatchField("count") + .build(); + + expectThrows(IllegalArgumentException.class, () -> TermsSetQueryBuilderProtoUtils.fromProto(proto)); + } + + public void testFromProtoWithBothFieldAndScript() { + Script script = Script.newBuilder() + .setInline( + InlineScript.newBuilder() + .setSource("params.required_count") + .setLang(ScriptLanguage.newBuilder().setBuiltin(BuiltinScriptLanguage.BUILTIN_SCRIPT_LANGUAGE_PAINLESS)) + .build() + ) + .build(); + + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("field_with_script") + .addTerms("term1") + .setMinimumShouldMatchField("field_count") // Both field and script + .setMinimumShouldMatchScript(script) + .build(); + + // OpenSearch TermsSetQueryBuilder doesn't allow both field and script to be set + // This should throw an IllegalArgumentException + expectThrows(IllegalArgumentException.class, () -> TermsSetQueryBuilderProtoUtils.fromProto(proto)); + } + + public void testFromProtoWithEmptyQueryName() { + TermsSetQuery proto = TermsSetQuery.newBuilder() + .setField("query_name_field") + .addTerms("term") + .setXName("") // Empty query name + .setMinimumShouldMatchField("count") + .build(); + + TermsSetQueryBuilder result = TermsSetQueryBuilderProtoUtils.fromProto(proto); + + assertNotNull(result); + assertEquals("", result.queryName()); // Empty string should be preserved + } + +}