From 2136ae862442e60d8afc1e48b8fdec254ea1d854 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 18 Jul 2023 19:16:21 -0700 Subject: [PATCH 01/12] Add `text` type. Signed-off-by: Yury-Fridlyand --- .../opensearch/sql/ast/expression/Cast.java | 2 + .../sql/data/type/ExprCoreType.java | 6 ++- .../opensearch/sql/data/type/ExprType.java | 10 ++++ .../sql/data/type/WideningTypeRule.java | 4 +- .../function/BuiltinFunctionName.java | 1 + .../operator/convert/TypeCastOperator.java | 8 +++ .../org/opensearch/sql/sql/ConditionalIT.java | 4 +- .../data/type/OpenSearchDataType.java | 52 ++++++++++++++++--- .../data/type/OpenSearchDateType.java | 18 ++++++- .../data/type/OpenSearchTextType.java | 35 ++++++------- .../opensearch/storage/OpenSearchIndex.java | 2 +- .../dsl/AggregationBuilderHelper.java | 3 +- .../dsl/BucketAggregationBuilder.java | 5 +- .../storage/script/core/ExpressionScript.java | 2 +- .../script/filter/lucene/LikeQuery.java | 5 +- .../script/filter/lucene/LuceneQuery.java | 1 + .../script/filter/lucene/TermQuery.java | 13 ++--- .../storage/script/sort/SortQueryBuilder.java | 2 +- 18 files changed, 124 insertions(+), 49 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java b/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java index 9121dbd87c..c390a93394 100644 --- a/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java +++ b/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java @@ -16,6 +16,7 @@ import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_LONG; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_SHORT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_STRING; +import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_TEXT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_TIME; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_TIMESTAMP; @@ -45,6 +46,7 @@ public class Cast extends UnresolvedExpression { private static final Map CONVERTED_TYPE_FUNCTION_NAME_MAP = new ImmutableMap.Builder() .put("string", CAST_TO_STRING.getName()) + .put("text", CAST_TO_TEXT.getName()) // TODO do we need this? .put("byte", CAST_TO_BYTE.getName()) .put("short", CAST_TO_SHORT.getName()) .put("int", CAST_TO_INT.getName()) diff --git a/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java b/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java index 815f94a9df..5510a7e89c 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java +++ b/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java @@ -44,7 +44,11 @@ public enum ExprCoreType implements ExprType { /** * String. */ - STRING(UNDEFINED), + TEXT(UNDEFINED), + STRING(TEXT), + // TODO why not + //STRING(UNDEFINED), + //TEXT(STRING), /** * Boolean. diff --git a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java index 782714ba70..7dd3f1a669 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java +++ b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java @@ -64,4 +64,14 @@ default List getParent() { default String legacyTypeName() { return typeName(); } + + // TODO doc + default String convertFieldForSearchQuery(String fieldName) { + return fieldName; + } + + // TODO doc + default Object convertValueForSearchQuery(ExprValue value) { + return value.value(); + } } diff --git a/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java b/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java index e1f356782f..22144783bd 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java +++ b/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java @@ -41,9 +41,9 @@ public static int distance(ExprType type1, ExprType type2) { } private static int distance(ExprType type1, ExprType type2, int distance) { - if (type1 == type2) { + if (type1.equals(type2)) { return distance; - } else if (type1 == UNKNOWN) { + } else if (type1.equals(UNKNOWN)) { return IMPOSSIBLE_WIDENING; } else { return type1.getParent().stream() diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index 728712f537..b990bfbda5 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -235,6 +235,7 @@ public enum BuiltinFunctionName { * Data Type Convert Function. */ CAST_TO_STRING(FunctionName.of("cast_to_string")), + CAST_TO_TEXT(FunctionName.of("cast_to_text")), CAST_TO_BYTE(FunctionName.of("cast_to_byte")), CAST_TO_SHORT(FunctionName.of("cast_to_short")), CAST_TO_INT(FunctionName.of("cast_to_int")), diff --git a/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java b/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java index d3295a53f0..7bd9443293 100644 --- a/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java +++ b/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java @@ -16,6 +16,7 @@ import static org.opensearch.sql.data.type.ExprCoreType.LONG; import static org.opensearch.sql.data.type.ExprCoreType.SHORT; import static org.opensearch.sql.data.type.ExprCoreType.STRING; +import static org.opensearch.sql.data.type.ExprCoreType.TEXT; import static org.opensearch.sql.data.type.ExprCoreType.TIME; import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.opensearch.sql.expression.function.FunctionDSL.impl; @@ -51,6 +52,7 @@ public class TypeCastOperator { */ public static void register(BuiltinFunctionRepository repository) { repository.register(castToString()); + repository.register(castToText()); repository.register(castToByte()); repository.register(castToShort()); repository.register(castToInt()); @@ -78,6 +80,12 @@ private static DefaultFunctionResolver castToString() { ); } + private static DefaultFunctionResolver castToText() { + return FunctionDSL.define(BuiltinFunctionName.CAST_TO_TEXT.getName(), + impl(nullMissingHandling(v -> v), TEXT, STRING) + ); + } + private static DefaultFunctionResolver castToByte() { return FunctionDSL.define(BuiltinFunctionName.CAST_TO_BYTE.getName(), impl(nullMissingHandling( diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java index 9a833e0aa1..162ce1ae26 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java @@ -60,8 +60,8 @@ public void ifnullWithNullInputTest() { + " WHERE balance is null limit 2", "jdbc")); verifySchema(response, - schema("IFNULL(null, firstname)", "IFNULL1", "keyword"), - schema("IFNULL(firstname, null)", "IFNULL2", "keyword"), + schema("IFNULL(null, firstname)", "IFNULL1", "text"), + schema("IFNULL(firstname, null)", "IFNULL2", "text"), schema("IFNULL(null, null)", "IFNULL3", "byte")); verifyDataRows(response, rows("Hattie", "Hattie", LITERAL_NULL.value()), diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java index 273b980d2a..b3c052155b 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java @@ -10,9 +10,10 @@ import java.io.Serializable; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.BiConsumer; -import lombok.EqualsAndHashCode; import lombok.Getter; import org.apache.commons.lang3.EnumUtils; import org.opensearch.sql.data.type.ExprCoreType; @@ -21,15 +22,51 @@ /** * The extension of ExprType in OpenSearch. */ -@EqualsAndHashCode public class OpenSearchDataType implements ExprType, Serializable { + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (o instanceof ExprCoreType) { + return exprCoreType.equals(o); + } + if (!(o instanceof OpenSearchDataType)) { + return false; + } + return exprCoreType.equals(((OpenSearchDataType) o).exprCoreType); + } + + public int hashCode() { + return 42 + exprCoreType.hashCode(); + } + + @Override + public List getParent() { + return exprCoreType == ExprCoreType.UNKNOWN + ? List.of(ExprCoreType.UNKNOWN) + : exprCoreType.getParent(); + } + + @Override + public boolean shouldCast(ExprType other) { + ExprCoreType otherCoreType = other instanceof ExprCoreType ? (ExprCoreType) other + : (other instanceof OpenSearchDataType + ? ((OpenSearchDataType) other).exprCoreType : ExprCoreType.UNKNOWN); + // TODO Copied from BuiltinFunctionRepository.isCastRequired + if (ExprCoreType.numberTypes().contains(exprCoreType) + && ExprCoreType.numberTypes().contains(otherCoreType)) { + return false; + } + return exprCoreType == ExprCoreType.UNKNOWN || exprCoreType.shouldCast(other); + } + /** * The mapping (OpenSearch engine) type. */ public enum MappingType { Invalid(null, ExprCoreType.UNKNOWN), - Text("text", ExprCoreType.UNKNOWN), + Text("text", ExprCoreType.TEXT), Keyword("keyword", ExprCoreType.STRING), Ip("ip", ExprCoreType.UNKNOWN), GeoPoint("geo_point", ExprCoreType.UNKNOWN), @@ -64,7 +101,6 @@ public String toString() { } } - @EqualsAndHashCode.Exclude @Getter protected MappingType mappingType; @@ -124,6 +160,9 @@ public static Map parseMapping(Map i return; } // create OpenSearchDataType + + // TODO parse `fielddata` + result.put(k, OpenSearchDataType.of( EnumUtils.getEnumIgnoreCase(OpenSearchDataType.MappingType.class, type), innerMap) @@ -212,7 +251,6 @@ protected OpenSearchDataType(ExprCoreType type) { // For datatypes with properties (example: object and nested types) // a read-only collection @Getter - @EqualsAndHashCode.Exclude Map properties = ImmutableMap.of(); @Override @@ -220,7 +258,7 @@ protected OpenSearchDataType(ExprCoreType type) { public String typeName() { // To avoid breaking changes return `string` for `typeName` call (PPL) and `text` for // `legacyTypeName` call (SQL). See more: https://github.com/opensearch-project/sql/issues/1296 - if (legacyTypeName().equals("TEXT")) { + if (legacyTypeName().equals("TEXT") || legacyTypeName().equals("KEYWORD")) { return "STRING"; } return legacyTypeName(); @@ -248,7 +286,7 @@ protected OpenSearchDataType cloneEmpty() { /** * Flattens mapping tree into a single layer list of objects (pairs of name-types actually), * which don't have nested types. - * See {@link OpenSearchDataTypeTest#traverseAndFlatten() test} for example. + * See {@see OpenSearchDataTypeTest#traverseAndFlatten() test} for example. * @param tree A list of `OpenSearchDataType`s - map between field name and its type. * @return A list of all `OpenSearchDataType`s from given map on the same nesting level (1). * Nested object names are prefixed by names of their host. diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java index 76947bf720..384b9e6b88 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java @@ -17,13 +17,13 @@ import lombok.EqualsAndHashCode; import org.opensearch.common.time.DateFormatter; import org.opensearch.common.time.FormatNames; +import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.data.type.ExprType; /** * Date type with support for predefined and custom formats read from the index mapping. */ -@EqualsAndHashCode(callSuper = true) public class OpenSearchDateType extends OpenSearchDataType { private static final OpenSearchDateType instance = new OpenSearchDateType(); @@ -403,4 +403,20 @@ protected OpenSearchDataType cloneEmpty() { } return OpenSearchDateType.of(String.join(" || ", formats)); } + + @Override + public String typeName() { + return exprCoreType.toString(); + } + + @Override + public String legacyTypeName() { + return exprCoreType.toString(); + } + + @Override + public Object convertValueForSearchQuery(ExprValue value) { + // TODO fix for https://github.com/opensearch-project/sql/issues/1847 + return value.timestampValue().toEpochMilli(); + } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java index 67b7296834..baf12c77ac 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java @@ -6,12 +6,13 @@ package org.opensearch.sql.opensearch.data.type; import static org.opensearch.sql.data.type.ExprCoreType.STRING; -import static org.opensearch.sql.data.type.ExprCoreType.UNKNOWN; +import static org.opensearch.sql.data.type.ExprCoreType.TEXT; import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.Map; -import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprType; /** @@ -24,18 +25,18 @@ public class OpenSearchTextType extends OpenSearchDataType { // text could have fields // a read-only collection - @EqualsAndHashCode.Exclude + @Getter Map fields = ImmutableMap.of(); private OpenSearchTextType() { super(MappingType.Text); - exprCoreType = UNKNOWN; + exprCoreType = TEXT; } /** * Constructs a Text Type using the passed in fields argument. * @param fields The fields to be used to construct the text type. - * @return A new OpenSeachTextTypeObject + * @return A new OpenSearchTextType object */ public static OpenSearchTextType of(Map fields) { var res = new OpenSearchTextType(); @@ -57,24 +58,22 @@ public boolean shouldCast(ExprType other) { return false; } - public Map getFields() { - return fields; - } - @Override protected OpenSearchDataType cloneEmpty() { return OpenSearchTextType.of(Map.copyOf(this.fields)); } - /** - * Text field doesn't have doc value (exception thrown even when you call "get") - * Limitation: assume inner field name is always "keyword". - */ - public static String convertTextToKeyword(String fieldName, ExprType fieldType) { - if (fieldType instanceof OpenSearchTextType - && ((OpenSearchTextType) fieldType).getFields().size() > 0) { - return fieldName + ".keyword"; + @Override + public String convertFieldForSearchQuery(String fieldName) { + if (fields.size() > 1) { + // TODO or pick first? + throw new RuntimeException("too many text fields"); + } + if (fields.size() == 0) { + return fieldName; } - return fieldName; + // TODO what if field is not a keyword + // https://github.com/opensearch-project/sql/issues/1112 + return String.format("%s.%s", fieldName, fields.keySet().toArray()[0]); } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java index 62617f744e..70055e9dfc 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java @@ -127,7 +127,7 @@ public Map getFieldTypes() { cachedFieldTypes = OpenSearchDataType.traverseAndFlatten(cachedFieldOpenSearchTypes) .entrySet().stream().collect( LinkedHashMap::new, - (map, item) -> map.put(item.getKey(), item.getValue().getExprType()), + (map, item) -> map.put(item.getKey(), item.getValue()/*.getExprType()*/), Map::putAll); } return cachedFieldTypes; diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java index 156b565976..a914ce757e 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java @@ -38,8 +38,7 @@ public T build(Expression expression, Function fieldBuilder, Function scriptBuilder) { if (expression instanceof ReferenceExpression) { String fieldName = ((ReferenceExpression) expression).getAttr(); - return fieldBuilder.apply( - OpenSearchTextType.convertTextToKeyword(fieldName, expression.type())); + return fieldBuilder.apply(expression.type().convertFieldForSearchQuery(fieldName)); } else if (expression instanceof FunctionExpression || expression instanceof LiteralExpression) { return scriptBuilder.apply(new Script( diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java index 1a6a82be96..8a2638ae85 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java @@ -12,6 +12,8 @@ import com.google.common.collect.ImmutableList; import java.util.List; +import java.util.stream.Stream; + import org.apache.commons.lang3.tuple.Triple; import org.opensearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; import org.opensearch.search.aggregations.bucket.composite.DateHistogramValuesSourceBuilder; @@ -71,7 +73,8 @@ private CompositeValuesSourceBuilder buildCompositeValuesSourceBuilder( .missingOrder(missingOrder) .order(sortOrder); // Time types values are converted to LONG in ExpressionAggregationScript::execute - if (List.of(TIMESTAMP, TIME, DATE, DATETIME).contains(expr.getDelegated().type())) { + if (Stream.of(TIMESTAMP, TIME, DATE, DATETIME) + .anyMatch(t -> expr.getDelegated().type().equals(t))) { sourceBuilder.userValuetypeHint(ValueType.LONG); } return helper.build(expr.getDelegated(), sourceBuilder::field, sourceBuilder::script); diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java index 9bdb15d63a..4dd4a4862b 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java @@ -131,7 +131,7 @@ private Environment buildValueEnv( private Object getDocValue(ReferenceExpression field, Supplier>> docProvider) { - String fieldName = OpenSearchTextType.convertTextToKeyword(field.getAttr(), field.type()); + String fieldName = field.type().convertFieldForSearchQuery(field.getAttr()); ScriptDocValues docValue = docProvider.get().get(fieldName); if (docValue == null || docValue.isEmpty()) { return null; // No way to differentiate null and missing from doc value diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java index 699af4f3fd..41a9a2d7f8 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java @@ -16,8 +16,9 @@ public class LikeQuery extends LuceneQuery { @Override public QueryBuilder doBuild(String fieldName, ExprType fieldType, ExprValue literal) { - String field = OpenSearchTextType.convertTextToKeyword(fieldName, fieldType); - return createBuilder(field, literal.stringValue()); + return createBuilder( + fieldType.convertFieldForSearchQuery(fieldName), + literal.stringValue()); } /** diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java index a45c535383..8eff50950f 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java @@ -126,6 +126,7 @@ private ExprValue cast(FunctionExpression castFunction) { return expr.valueOf(); } }) + // TODO CAST_TO_TEXT .put(BuiltinFunctionName.CAST_TO_BYTE.getName(), expr -> { if (ExprCoreType.numberTypes().contains(expr.type())) { return new ExprByteValue(expr.valueOf().byteValue()); diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java index c98de1cd84..242402d30c 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java @@ -20,15 +20,8 @@ public class TermQuery extends LuceneQuery { @Override protected QueryBuilder doBuild(String fieldName, ExprType fieldType, ExprValue literal) { - fieldName = OpenSearchTextType.convertTextToKeyword(fieldName, fieldType); - return QueryBuilders.termQuery(fieldName, value(literal)); - } - - private Object value(ExprValue literal) { - if (literal.type().equals(ExprCoreType.TIMESTAMP)) { - return literal.timestampValue().toEpochMilli(); - } else { - return literal.value(); - } + return QueryBuilders.termQuery( + fieldType.convertFieldForSearchQuery(fieldName), + fieldType.convertValueForSearchQuery(literal)); } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java index 62c923832c..22848036e1 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java @@ -99,7 +99,7 @@ private void validateNestedArgs(FunctionExpression nestedFunc) { private FieldSortBuilder fieldBuild(ReferenceExpression ref, Sort.SortOption option) { return SortBuilders.fieldSort( - OpenSearchTextType.convertTextToKeyword(ref.getAttr(), ref.type())) + ref.type().convertFieldForSearchQuery(ref.getAttr())) .order(sortOrderMap.get(option.getSortOrder())) .missing(missingMap.get(option.getNullOrder())); } From bb9f30e793b38fed63047deb12e40bdec23e3737 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 20 Jul 2023 19:18:56 -0700 Subject: [PATCH 02/12] Simplify implementation. Signed-off-by: Yury-Fridlyand --- .../java/org/opensearch/sql/ast/expression/Cast.java | 2 -- .../java/org/opensearch/sql/data/type/ExprCoreType.java | 6 +----- .../sql/expression/function/BuiltinFunctionName.java | 1 - .../expression/operator/convert/TypeCastOperator.java | 9 +-------- .../sql/opensearch/data/type/OpenSearchDataType.java | 9 ++++++--- .../sql/opensearch/data/type/OpenSearchTextType.java | 4 +--- .../storage/script/filter/lucene/LuceneQuery.java | 1 - 7 files changed, 9 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java b/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java index c390a93394..9121dbd87c 100644 --- a/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java +++ b/core/src/main/java/org/opensearch/sql/ast/expression/Cast.java @@ -16,7 +16,6 @@ import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_LONG; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_SHORT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_STRING; -import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_TEXT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_TIME; import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_TIMESTAMP; @@ -46,7 +45,6 @@ public class Cast extends UnresolvedExpression { private static final Map CONVERTED_TYPE_FUNCTION_NAME_MAP = new ImmutableMap.Builder() .put("string", CAST_TO_STRING.getName()) - .put("text", CAST_TO_TEXT.getName()) // TODO do we need this? .put("byte", CAST_TO_BYTE.getName()) .put("short", CAST_TO_SHORT.getName()) .put("int", CAST_TO_INT.getName()) diff --git a/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java b/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java index 5510a7e89c..815f94a9df 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java +++ b/core/src/main/java/org/opensearch/sql/data/type/ExprCoreType.java @@ -44,11 +44,7 @@ public enum ExprCoreType implements ExprType { /** * String. */ - TEXT(UNDEFINED), - STRING(TEXT), - // TODO why not - //STRING(UNDEFINED), - //TEXT(STRING), + STRING(UNDEFINED), /** * Boolean. diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index b990bfbda5..728712f537 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -235,7 +235,6 @@ public enum BuiltinFunctionName { * Data Type Convert Function. */ CAST_TO_STRING(FunctionName.of("cast_to_string")), - CAST_TO_TEXT(FunctionName.of("cast_to_text")), CAST_TO_BYTE(FunctionName.of("cast_to_byte")), CAST_TO_SHORT(FunctionName.of("cast_to_short")), CAST_TO_INT(FunctionName.of("cast_to_int")), diff --git a/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java b/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java index 7bd9443293..6dac9c300a 100644 --- a/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java +++ b/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java @@ -16,7 +16,7 @@ import static org.opensearch.sql.data.type.ExprCoreType.LONG; import static org.opensearch.sql.data.type.ExprCoreType.SHORT; import static org.opensearch.sql.data.type.ExprCoreType.STRING; -import static org.opensearch.sql.data.type.ExprCoreType.TEXT; +//import static org.opensearch.sql.data.type.ExprCoreType.TEXT; import static org.opensearch.sql.data.type.ExprCoreType.TIME; import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.opensearch.sql.expression.function.FunctionDSL.impl; @@ -52,7 +52,6 @@ public class TypeCastOperator { */ public static void register(BuiltinFunctionRepository repository) { repository.register(castToString()); - repository.register(castToText()); repository.register(castToByte()); repository.register(castToShort()); repository.register(castToInt()); @@ -80,12 +79,6 @@ private static DefaultFunctionResolver castToString() { ); } - private static DefaultFunctionResolver castToText() { - return FunctionDSL.define(BuiltinFunctionName.CAST_TO_TEXT.getName(), - impl(nullMissingHandling(v -> v), TEXT, STRING) - ); - } - private static DefaultFunctionResolver castToByte() { return FunctionDSL.define(BuiltinFunctionName.CAST_TO_BYTE.getName(), impl(nullMissingHandling( diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java index b3c052155b..67929d76ce 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java @@ -12,7 +12,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.BiConsumer; import lombok.Getter; import org.apache.commons.lang3.EnumUtils; @@ -34,7 +33,11 @@ public boolean equals(final Object o) { if (!(o instanceof OpenSearchDataType)) { return false; } - return exprCoreType.equals(((OpenSearchDataType) o).exprCoreType); + OpenSearchDataType other = (OpenSearchDataType) o; + if (mappingType != null && other.mappingType != null) { + return mappingType.equals(other.mappingType); + } + return exprCoreType.equals(other.exprCoreType); } public int hashCode() { @@ -66,7 +69,7 @@ public boolean shouldCast(ExprType other) { */ public enum MappingType { Invalid(null, ExprCoreType.UNKNOWN), - Text("text", ExprCoreType.TEXT), + Text("text", ExprCoreType.STRING), Keyword("keyword", ExprCoreType.STRING), Ip("ip", ExprCoreType.UNKNOWN), GeoPoint("geo_point", ExprCoreType.UNKNOWN), diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java index baf12c77ac..012b001245 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java @@ -6,13 +6,11 @@ package org.opensearch.sql.opensearch.data.type; import static org.opensearch.sql.data.type.ExprCoreType.STRING; -import static org.opensearch.sql.data.type.ExprCoreType.TEXT; import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.Map; import lombok.Getter; -import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprType; /** @@ -30,7 +28,7 @@ public class OpenSearchTextType extends OpenSearchDataType { private OpenSearchTextType() { super(MappingType.Text); - exprCoreType = TEXT; + exprCoreType = STRING; } /** diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java index 8eff50950f..a45c535383 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LuceneQuery.java @@ -126,7 +126,6 @@ private ExprValue cast(FunctionExpression castFunction) { return expr.valueOf(); } }) - // TODO CAST_TO_TEXT .put(BuiltinFunctionName.CAST_TO_BYTE.getName(), expr -> { if (ExprCoreType.numberTypes().contains(expr.type())) { return new ExprByteValue(expr.valueOf().byteValue()); From 33370ff8f2ab48cf1e2e54b8b39653d7adce897e Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 20 Jul 2023 20:05:32 -0700 Subject: [PATCH 03/12] Add docs. Signed-off-by: Yury-Fridlyand --- docs/dev/img/type-hierarchy-tree-final.png | Bin 0 -> 30134 bytes docs/dev/index.md | 1 + docs/dev/query-type-conversion.md | 34 +++++++++++------- docs/dev/text-type.md | 38 +++++++++++++++++++++ 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 docs/dev/img/type-hierarchy-tree-final.png create mode 100644 docs/dev/text-type.md diff --git a/docs/dev/img/type-hierarchy-tree-final.png b/docs/dev/img/type-hierarchy-tree-final.png new file mode 100644 index 0000000000000000000000000000000000000000..68309c5cf9c9453d1195baca9aea3a7aaae381f9 GIT binary patch literal 30134 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

a})) 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 DATE('2020-06-01') => date > '2020-06-01'` -2. *Schema-on-read*: More importantly, implicit conversion from string is required for schema on read (stored as raw string on write and extract field(s) on read), ex. `regex ‘...’ | abs(a)` +1. *User-friendly*: Although it doesn't matter for application or BI tool which can always follow the strict grammar rule, it's more friendly and accessible to human by implicit type conversion, ex. `date > DATE('2020-06-01') => date > '2020-06-01'` +2. *Schema-on-read*: More importantly, implicit conversion from string is required for schema on read (stored as raw string on write and extract field(s) on read), ex. `regex ‘...' | abs(a)` ### 2.2 Functionalities @@ -50,7 +50,7 @@ Future: ### 3.1 Type Precedence -Type precedence determines the direction of conversion when fields involved in an expression has different type from resolved signature. Before introducing it into our type system, let’s check how an expression is resolved to a function implementation and why type precedence is required. +Type precedence determines the direction of conversion when fields involved in an expression has different type from resolved signature. Before introducing it into our type system, let's check how an expression is resolved to a function implementation and why type precedence is required. ``` Compiling time: @@ -60,7 +60,7 @@ Compiling time: Function builder: returns equal(DOUBLE, DOUBLE) impl ``` -Now let’s follow the same idea to add support for conversion from `BOOLEAN` to `STRING`. Because all boolean values can be converted to a string (in other word string is “wider”), String type is made the parent of Boolean. However, this leads to wrong semantic as the following expression `false = ‘FALSE’` for example: +Now let's follow the same idea to add support for conversion from `BOOLEAN` to `STRING`. Because all boolean values can be converted to a string (in other word string is 'wider'), String type is made the parent of Boolean. However, this leads to wrong semantic as the following expression `false = 'FALSE'` for example: ``` Compiling time: @@ -74,9 +74,10 @@ Runtime: Evaluation result: *false* ``` -Therefore type precedence is supposed to be defined based on semantic expected rather than intuitive “width” of type. Now let’s reverse the direction and make Boolean the parent of String type. +Therefore type precedence is supposed to be defined based on semantic expected rather than intuitive 'width' of type. Now let's reverse the direction and make Boolean the parent of String type. ![New type hierarchy](img/type-hierarchy-tree-with-implicit-cast.png) +Note: type hierarchy structure shown on the picture below was implemented in [#166](https://github.com/opensearch-project/sql/pull/166), but was changed later. ``` Compiling time: @@ -84,7 +85,7 @@ Compiling time: Unresolved signature: equal(BOOL, STRING) Resovled signature: equal(BOOL, BOOL) Function builder: 1) returns equal(BOOL, cast_to_bool(STRING)) impl - 2) returns equal(BOOL, BOOL) impl + 1) returns equal(BOOL, BOOL) impl Runtime: equal impl: false.equals(cast_to_bool('FALSE')) cast_to_bool impl: Boolean.valueOf('FALSE') @@ -147,15 +148,15 @@ public enum ExprCoreType implements ExprType { DOUBLE(FLOAT), STRING(UNDEFINED), - BOOLEAN(STRING), // PR: change STRING's parent to BOOLEAN + BOOLEAN(STRING), // #166 changes: STRING's parent to BOOLEAN /** * Date. */ - TIMESTAMP(UNDEFINED), - DATE(UNDEFINED), - TIME(UNDEFINED), - DATETIME(UNDEFINED), + DATE(STRING), // #171 changes: datetime types parent to STRING + TIME(STRING), + DATETIME(STRING, DATE, TIME), // #1196 changes: extend DATETIME and TIMESTAMP parent list + TIMESTAMP(STRING, DATETIME), INTERVAL(UNDEFINED), STRUCT(UNDEFINED), @@ -170,3 +171,12 @@ As with examples in section 3.1, the implementation is: 1. Define all possible conversions in CAST function family. 2. Define implicit conversions by type hierarchy tree (auto implicit cast from child to parent) 3. During compile time, wrap original function builder by a new one which cast arguments to target type. + +## Final type hierarchy scheme + +![Most relevant type hierarchy](img/type-hierarchy-tree-final.png) + +## References +* [#166](https://github.com/opensearch-project/sql/pull/166): Add automatic `STRING` -> `BOOLEAN` cast +* [#171](https://github.com/opensearch-project/sql/pull/171): Add automatic `STRING` -> `DATE`/`TIME`/`DATETIME`/`TIMESTAMP` cast +* [#1196](https://github.com/opensearch-project/sql/pull/1196): Add automatic casts between datetime types `DATE`/`TIME`/`DATETIME` -> `DATE`/`TIME`/`DATETIME`/`TIMESTAMP` diff --git a/docs/dev/text-type.md b/docs/dev/text-type.md new file mode 100644 index 0000000000..9fc2033e2a --- /dev/null +++ b/docs/dev/text-type.md @@ -0,0 +1,38 @@ +## History overview + +* [odfe#620](https://github.com/opendistro-for-elasticsearch/sql/pull/620) - added two types for `text` (just text and text with fields), querying and string functions are supported, but searching is not. +* [odfe#682](https://github.com/opendistro-for-elasticsearch/sql/pull/682) and [odfe#730](https://github.com/opendistro-for-elasticsearch/sql/pull/730) - added support for querying (in 682) and aggregation (730); if `text` has sub-fields, `.keyword` prefix is added to the field name regardless of the sub-field names. +* [#1314](https://github.com/opensearch-project/sql/pull/1314) and [#1664](https://github.com/opensearch-project/sql/pull/1664) - changed format of storing `text` type; it is one type now, subfield information is stored, but not yet used. +* Current changes (no PR number yet) - correctly resolve sub-field name if a `text` field has only one subfield. Fixes [#1112](https://github.com/opensearch-project/sql/issues/1112) and [#1038](https://github.com/opensearch-project/sql/issues/1038). + +## Further changes + +* Support search for text sub-fields ([#1113](https://github.com/opensearch-project/sql/issues/1113)). +* Support multiple sub-fields for text ([#1887](https://github.com/opensearch-project/sql/issues/1887)). +* Support non-default date formats for search queries ([#1847](https://github.com/opensearch-project/sql/issues/1847)). Fix for this bug depends on the current changes. + +## Problem statement + +`:opensearch` module parses index mapping and builds instances of `OpenSearchDataType` (a base class), but ships simplified types (`ExprCoreType` - a enum) to `:core` module, because `:core` uses `ExprCoreType`s to resolve functions. + +```mermaid +sequenceDiagram + participant core as :core + participant opensearch as :opensearch + + core ->>+ opensearch : resolve types + opensearch ->>- core : simplified types + note over core: preparing query + core ->> opensearch : translate request into datasource DSL +``` + +Later, `:core` returns to `:opensearch` the DSL request with types stored. Since types were simplified, all mapping information is lost. Adding new `TEXT` entry to `ExprCoreType` enum is not enough, because `ExprCoreType` is datasource agnostic and can't store any specific mapping info. + +## Solution + +The solution is to provide to `:core` non simplified types, but full types. Those objects should be fully compatible with `ExprCoreType` and implement all required APIs to allow `:core` to manipulate with built-in functions. Once those type objects are returned back to `:opensearch`, it can get all required information to build the correct search request. + +1. Pass full (non simplified) types to and through `:core`. +2. Update `OpenSearchDataType` (and inheritors if needed) to be comparable with `ExprCoreType`. +3. Update `:core` to do proper comparison (use `.equals` instead of `==`). +5. Update `:opensearch` to use the mapping information received from `:core` and properly build the search query. From 5b2a6599c1140ad36c03853e8e303eee70eb19c7 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 20 Jul 2023 20:08:53 -0700 Subject: [PATCH 04/12] Add IT. Signed-off-by: Yury-Fridlyand --- .../sql/legacy/SQLIntegTestCase.java | 11 +- .../opensearch/sql/legacy/TestsConstants.java | 2 + .../org/opensearch/sql/sql/TextTypeIT.java | 333 ++++++++++++++++++ .../text_for_cast_index_mappings.json | 56 +++ .../indexDefinitions/text_index_mappings.json | 56 +++ integ-test/src/test/resources/text.json | 10 + .../src/test/resources/text_for_cast.json | 14 + 7 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 integ-test/src/test/java/org/opensearch/sql/sql/TextTypeIT.java create mode 100644 integ-test/src/test/resources/indexDefinitions/text_for_cast_index_mappings.json create mode 100644 integ-test/src/test/resources/indexDefinitions/text_index_mappings.json create mode 100644 integ-test/src/test/resources/text.json create mode 100644 integ-test/src/test/resources/text_for_cast.json diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java b/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java index 7216c03d08..3b79c497f7 100644 --- a/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java +++ b/integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java @@ -681,7 +681,16 @@ public enum Index { NESTED_WITH_NULLS(TestsConstants.TEST_INDEX_NESTED_WITH_NULLS, "multi_nested", getNestedTypeIndexMapping(), - "src/test/resources/nested_with_nulls.json"); + "src/test/resources/nested_with_nulls.json"), + TEXT(TestsConstants.TEST_INDEX_TEXT, + "text", + getMappingFile("text_index_mappings.json"), + "src/test/resources/text.json"), + TEXT_FOR_CAST(TestsConstants.TEST_INDEX_TEXT_FOR_CAST, + "text", + getMappingFile("text_for_cast_index_mappings.json"), + "src/test/resources/text_for_cast.json"), + ; private final String name; private final String type; diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java b/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java index 338be25a0c..4edfaf4dc2 100644 --- a/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java +++ b/integ-test/src/test/java/org/opensearch/sql/legacy/TestsConstants.java @@ -60,6 +60,8 @@ public class TestsConstants { public final static String TEST_INDEX_WILDCARD = TEST_INDEX + "_wildcard"; public final static String TEST_INDEX_MULTI_NESTED_TYPE = TEST_INDEX + "_multi_nested"; public final static String TEST_INDEX_NESTED_WITH_NULLS = TEST_INDEX + "_nested_with_nulls"; + public final static String TEST_INDEX_TEXT = TEST_INDEX + "_text"; + public final static String TEST_INDEX_TEXT_FOR_CAST = TEST_INDEX + "_text_for_cast"; public final static String DATASOURCES = ".ql-datasources"; public final static String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/TextTypeIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/TextTypeIT.java new file mode 100644 index 0000000000..ce43dcf2af --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/sql/TextTypeIT.java @@ -0,0 +1,333 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.sql; + +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_TEXT; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_TEXT_FOR_CAST; +import static org.opensearch.sql.legacy.plugin.RestSqlAction.QUERY_API_ENDPOINT; +import static org.opensearch.sql.util.MatcherUtils.rows; +import static org.opensearch.sql.util.MatcherUtils.schema; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; +import static org.opensearch.sql.util.MatcherUtils.verifySchema; +import static org.opensearch.sql.util.TestUtils.getResponseBody; + +import java.util.Locale; +import lombok.SneakyThrows; +import org.json.JSONObject; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.opensearch.client.Request; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.Response; +import org.opensearch.sql.legacy.SQLIntegTestCase; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public class TextTypeIT extends SQLIntegTestCase { + + @Override + public void init() throws Exception { + loadIndex(Index.TEXT); + loadIndex(Index.TEXT_FOR_CAST); + } + + @Test + public void test_all_data_returned_as_strings() { + JSONObject result = executeQuery("select * FROM " + TEST_INDEX_TEXT); + verifySchema(result, + schema("TextWithKeywords", null, "text"), + schema("TextWithFielddata", null, "text"), + schema("Keyword", null, "keyword"), + schema("Text", null, "text"), + schema("TextWithFields", null, "text"), + schema("TextWithMixedFields", null, "text"), + schema("TextWithNumbers", null, "text") + ); + verifyDataRows(result, + rows("Text With Keywords", "Text", "K for Keyword", null, null, "one two tree", null), + rows(null, "Fielddata is the best!", null, "Text", "Fields for freedom", "42", "42"), + rows("Another Text With Similar Keywords", null, "ikiki", null, null, "12.04.1961", "42"), + rows("", "Text With Fielddata", null, "Some text", "Freedom for fields", null, "100500"), + rows(null, null, "kokoko", "A fairy tale about little mermaid", "A Text with a Field", null, null) + ); + } + + @Test + // TODO uncomment test ascii on TextWithKeywords (after https://github.com/Bit-Quill/opensearch-project-sql/pull/301) + public void test_ascii() { + //JSONObject result = executeQuery("select ascii(Keyword), ascii(TextWithKeywords), ascii(Text), ascii(TextWithFielddata), " + JSONObject result = executeQuery("select ascii(Keyword), ascii(Text), ascii(TextWithFielddata), " + + "ascii(TextWithFields), ascii(TextWithNumbers), ascii(TextWithMixedFields) FROM " + TEST_INDEX_TEXT); + + verifySchema(result, + schema("ascii(Keyword)", null, "integer"), + //schema("ascii(TextWithKeywords)", null, "integer"), + schema("ascii(Text)", null, "integer"), + schema("ascii(TextWithFielddata)", null, "integer"), + schema("ascii(TextWithFields)", null, "integer"), + schema("ascii(TextWithNumbers)", null, "integer"), + schema("ascii(TextWithMixedFields)", null, "integer") + ); + verifyDataRows(result, + rows(75, /*84,*/ null, 84, null, null, 111), + rows(null, /*null,*/ 84, 70, 70, 52, 52), + rows(105, /*65,*/ null, null, null, 52, 49), + rows(null, /*0,*/ 83, 84, 70, 49, null), + rows(107, /*null,*/ 65, null, 65, null, null) + ); + } + + @Test + public void test_concat_with_user_string() { + JSONObject result = executeQuery("select concat(Keyword), concat(Keyword, 'ab'), " + + "concat(TextWithKeywords), concat(TextWithKeywords, 'bc'), " + + "concat(Text), concat(Text, 'bc'), " + + "concat(TextWithFielddata), concat(TextWithFielddata, 'cd'), " + + "concat(TextWithFields), concat(TextWithFields, 'de'), " + + "concat(TextWithNumbers), concat(TextWithNumbers, 'ef'), " + + "concat(TextWithMixedFields), concat(TextWithMixedFields, 'fg') FROM " + TEST_INDEX_TEXT); + + verifySchema(result, + schema("concat(Keyword)", null, "keyword"), + schema("concat(Keyword, 'ab')", null, "keyword"), + schema("concat(TextWithKeywords)", null, "keyword"), + schema("concat(TextWithKeywords, 'bc')", null, "keyword"), + schema("concat(Text)", null, "keyword"), + schema("concat(Text, 'bc')", null, "keyword"), + schema("concat(TextWithFielddata)", null, "keyword"), + schema("concat(TextWithFielddata, 'cd')", null, "keyword"), + schema("concat(TextWithFields)", null, "keyword"), + schema("concat(TextWithFields, 'de')", null, "keyword"), + schema("concat(TextWithNumbers)", null, "keyword"), + schema("concat(TextWithNumbers, 'ef')", null, "keyword"), + schema("concat(TextWithMixedFields)", null, "keyword"), + schema("concat(TextWithMixedFields, 'fg')", null, "keyword") + ); + verifyDataRows(result, + rows("K for Keyword", "K for Keywordab", "Text With Keywords", "Text With Keywordsbc", null, null, + "Text", "Textcd", null, null, null, null, "one two tree", "one two treefg"), + rows(null, null, null, null, "Text", "Textbc", "Fielddata is the best!", "Fielddata is the best!cd", + "Fields for freedom", "Fields for freedomde", "42", "42ef", "42", "42fg"), + rows("ikiki", "ikikiab", "Another Text With Similar Keywords", "Another Text With Similar Keywordsbc", + null, null, null, null, null, null, "42", "42ef", "12.04.1961", "12.04.1961fg"), + rows(null, null, "", "bc", "Some text", "Some textbc", "Text With Fielddata", "Text With Fielddatacd", + "Freedom for fields", "Freedom for fieldsde", "100500", "100500ef", null, null), + rows("kokoko", "kokokoab", null, null, "A fairy tale about little mermaid", "A fairy tale about little mermaidbc", + null, null, "A Text with a Field", "A Text with a Fieldde", null, null, null, null) + ); + } + + @Test + public void test_concat_ws_with_different_types() { + JSONObject result = executeQuery("select concat_ws(' ', Keyword, Keyword) AS `KW + KW`, " + + "concat_ws(' ', TextWithKeywords, Keyword) AS `T_KW + KW`, " + + "concat_ws('__', TextWithFielddata, TextWithFields) AS `T_FD + T_F`, " + + "concat_ws('/', '|', TextWithNumbers) AS `U + U + T_N`, " + + "concat_WS(' == ', TextWithMixedFields, TextWithNumbers) AS `T_MX + T_N` FROM " + TEST_INDEX_TEXT); + + verifySchema(result, + schema("concat_ws(' ', Keyword, Keyword)", "KW + KW", "keyword"), + schema("concat_ws(' ', TextWithKeywords, Keyword)", "T_KW + KW", "keyword"), + schema("concat_ws('__', TextWithFielddata, TextWithFields)", "T_FD + T_F", "keyword"), + schema("concat_ws('/', '|', TextWithNumbers)", "U + U + T_N", "keyword"), + schema("concat_WS(' == ', TextWithMixedFields, TextWithNumbers)", "T_MX + T_N", "keyword") + ); + verifyDataRows(result, + rows("K for Keyword K for Keyword", "Text With Keywords K for Keyword", null, null, null), + rows(null, null, "Fielddata is the best!__Fields for freedom", "|/42", "42 == 42"), + rows("ikiki ikiki", "Another Text With Similar Keywords ikiki", null, "|/42", "12.04.1961 == 42"), + rows(null, null, "Text With Fielddata__Freedom for fields", "|/100500", null), + rows("kokoko kokoko", null, null, null, null) + ); + } + + @Test + public void test_simple_string_functions() { + JSONObject result = executeQuery("select left(Keyword, 4) as left, upper(TextWithKeywords) as upper, " + + "locate('t', Text) as locate, trim(TextWithFielddata) as trim, " + + "reverse(TextWithFields) as reverse, substring(TextWithNumbers, 5, 3) as substring, " + + "TextWithMixedFields = '42' as compare FROM " + TEST_INDEX_TEXT); + + verifySchema(result, + schema("left(Keyword, 4)", "left", "keyword"), + schema("upper(TextWithKeywords)", "upper", "keyword"), + schema("locate('t', Text)", "locate", "integer"), + schema("trim(TextWithFielddata)", "trim", "keyword"), + schema("reverse(TextWithFields)", "reverse", "keyword"), + schema("substring(TextWithNumbers, 5, 3)", "substring", "keyword"), + schema("TextWithMixedFields = '42'", "compare", "boolean") + ); + verifyDataRows(result, + rows("K fo", "TEXT WITH KEYWORDS", null, "Text", null, null, false), + rows(null, null, 4, "Fielddata is the best!", "modeerf rof sdleiF", "", true), + rows("ikik", "ANOTHER TEXT WITH SIMILAR KEYWORDS", null, null, null, "", false), + rows(null, "", 6, "Text With Fielddata", "sdleif rof modeerF", "00", null), + rows("koko", null, 9, null, "dleiF a htiw txeT A", null, null) + ); + } + + @Test + public void test_cast_text_to_bool() { + JSONObject result = executeQuery(String.format("select " + + "cast(TextWithKeywords as BOOLEAN) as TextWithKeywords, " + + "cast(TextWithFielddata as BOOLEAN) as TextWithFielddata, " + + "cast(TextWithNumbers as BOOLEAN) as TextWithNumbers, " + + "cast(TextWithMixedFields as BOOLEAN) as TextWithMixedFields, " + + "cast(Text as BOOLEAN) as Text, " + + "cast(TextWithFields as BOOLEAN) as TextWithFields " + + "from %s where TestName = 'bool';", TEST_INDEX_TEXT_FOR_CAST)); + + verifySchema(result, + schema("cast(TextWithKeywords as BOOLEAN)", "TextWithKeywords", "boolean"), + schema("cast(TextWithFielddata as BOOLEAN)", "TextWithFielddata", "boolean"), + schema("cast(TextWithNumbers as BOOLEAN)", "TextWithNumbers", "boolean"), + schema("cast(TextWithMixedFields as BOOLEAN)", "TextWithMixedFields", "boolean"), + schema("cast(Text as BOOLEAN)", "Text", "boolean"), + schema("cast(TextWithFields as BOOLEAN)", "TextWithFields", "boolean") + ); + verifyDataRows(result, + // int -> bool conversion doesn't fool C standard, so + // TextWithNumbers's value 42 converted to `false` + rows(true, true, false, true, true, true), + rows(false, false, false, false, false, false) + ); + } + + @Test + public void test_cast_text_to_int() { + JSONObject result = executeQuery(String.format("select " + + "cast(TextWithKeywords as INT) as TextWithKeywords, " + + "cast(TextWithFielddata as INT) as TextWithFielddata, " + + "cast(TextWithNumbers as INT) as TextWithNumbers, " + + "cast(TextWithMixedFields as INT) as TextWithMixedFields, " + + "cast(Text as INT) as Text, " + + "cast(TextWithFields as INT) as TextWithFields " + + "from %s where TestName = 'int';", TEST_INDEX_TEXT_FOR_CAST)); + + verifySchema(result, + schema("cast(TextWithKeywords as INT)", "TextWithKeywords", "integer"), + schema("cast(TextWithFielddata as INT)", "TextWithFielddata", "integer"), + schema("cast(TextWithNumbers as INT)", "TextWithNumbers", "integer"), + schema("cast(TextWithMixedFields as INT)", "TextWithMixedFields", "integer"), + schema("cast(Text as INT)", "Text", "integer"), + schema("cast(TextWithFields as INT)", "TextWithFields", "integer") + ); + verifyDataRows(result, + rows(1, -14, 100500, 42, 0, 4096) + ); + } + + @Test + public void test_cast_text_to_double() { + JSONObject result = executeQuery(String.format("select " + + "cast(TextWithKeywords as DOUBLE) as TextWithKeywords, " + + "cast(TextWithFielddata as DOUBLE) as TextWithFielddata, " + + "cast(TextWithNumbers as DOUBLE) as TextWithNumbers, " + + "cast(TextWithMixedFields as DOUBLE) as TextWithMixedFields, " + + "cast(Text as DOUBLE) as Text, " + + "cast(TextWithFields as DOUBLE) as TextWithFields " + + "from %s where TestName = 'double';", TEST_INDEX_TEXT_FOR_CAST)); + + verifySchema(result, + schema("cast(TextWithKeywords as DOUBLE)", "TextWithKeywords", "double"), + schema("cast(TextWithFielddata as DOUBLE)", "TextWithFielddata", "double"), + schema("cast(TextWithNumbers as DOUBLE)", "TextWithNumbers", "double"), + schema("cast(TextWithMixedFields as DOUBLE)", "TextWithMixedFields", "double"), + schema("cast(Text as DOUBLE)", "Text", "double"), + schema("cast(TextWithFields as DOUBLE)", "TextWithFields", "double") + ); + verifyDataRows(result, + rows(1.0, 3.14, -333.0, 42.0, .0, .223) + ); + } + + @Test + public void test_cast_text_to_date() { + // TextWithNumbers is excluded from the test, because we can't ingest the data + // Value "2011-11-22" for that field is not accepted, because OS tries to parse it as a number + JSONObject result = executeQuery(String.format("select " + + "cast(TextWithKeywords as DATE) as TextWithKeywords, " + + "cast(TextWithFielddata as DATE) as TextWithFielddata, " + + "cast(TextWithMixedFields as DATE) as TextWithMixedFields, " + + "cast(Text as DATE) as Text, " + + "cast(TextWithFields as DATE) as TextWithFields " + + "from %s where TestName = 'date';", TEST_INDEX_TEXT_FOR_CAST)); + + verifySchema(result, + schema("cast(TextWithKeywords as DATE)", "TextWithKeywords", "date"), + schema("cast(TextWithFielddata as DATE)", "TextWithFielddata", "date"), + schema("cast(TextWithMixedFields as DATE)", "TextWithMixedFields", "date"), + schema("cast(Text as DATE)", "Text", "date"), + schema("cast(TextWithFields as DATE)", "TextWithFields", "date") + ); + verifyDataRows(result, + rows("1984-04-04", "1999-10-20", "1977-07-17", "1961-04-12", "2020-02-29") + ); + } + + @Test + public void test_cast_text_to_time() { + // TextWithNumbers is excluded from the test, because we can't ingest the data + // Value "07:08:09" for that field is not accepted, because OS tries to parse it as a number + JSONObject result = executeQuery(String.format("select " + + "cast(TextWithKeywords as TIME) as TextWithKeywords, " + + "cast(TextWithFielddata as TIME) as TextWithFielddata, " + + "cast(TextWithMixedFields as TIME) as TextWithMixedFields, " + + "cast(Text as TIME) as Text, " + + "cast(TextWithFields as TIME) as TextWithFields " + + "from %s where TestName = 'time';", TEST_INDEX_TEXT_FOR_CAST)); + + verifySchema(result, + schema("cast(TextWithKeywords as TIME)", "TextWithKeywords", "time"), + schema("cast(TextWithFielddata as TIME)", "TextWithFielddata", "time"), + schema("cast(TextWithMixedFields as TIME)", "TextWithMixedFields", "time"), + schema("cast(Text as TIME)", "Text", "time"), + schema("cast(TextWithFields as TIME)", "TextWithFields", "time") + ); + + verifyDataRows(result, + rows("22:22:00", "00:00:00", "07:17:00", "10:20:30", "00:00:01") + ); + } + + @Test + public void test_cast_text_to_timestamp() { + // TextWithNumbers is excluded from the test for the same reasons (see above) + JSONObject result = executeQuery(String.format("select " + + "cast(TextWithKeywords as TIMESTAMP) as TextWithKeywords, " + + "cast(TextWithFielddata as TIMESTAMP) as TextWithFielddata, " + + "cast(TextWithMixedFields as TIMESTAMP) as TextWithMixedFields, " + + "cast(Text as TIMESTAMP) as Text, " + + "cast(TextWithFields as TIMESTAMP) as TextWithFields " + + "from %s where TestName = 'timestamp';", TEST_INDEX_TEXT_FOR_CAST)); + + verifySchema(result, + schema("cast(TextWithKeywords as TIMESTAMP)", "TextWithKeywords", "timestamp"), + schema("cast(TextWithFielddata as TIMESTAMP)", "TextWithFielddata", "timestamp"), + schema("cast(TextWithMixedFields as TIMESTAMP)", "TextWithMixedFields", "timestamp"), + schema("cast(Text as TIMESTAMP)", "Text", "timestamp"), + schema("cast(TextWithFields as TIMESTAMP)", "TextWithFields", "timestamp") + ); + + verifyDataRows(result, + rows("1984-04-04 10:20:30", "1999-10-20 00:01:02", "1977-07-17 01:02:03", + "1961-04-12 09:07:00", "2020-02-29 00:00:00") + ); + } + + @SneakyThrows + protected JSONObject executeQuery(String query) { + Request request = new Request("POST", QUERY_API_ENDPOINT); + request.setJsonEntity(String.format(Locale.ROOT, "{\n" + " \"query\": \"%s\"\n" + "}", query)); + + RequestOptions.Builder restOptionsBuilder = RequestOptions.DEFAULT.toBuilder(); + restOptionsBuilder.addHeader("Content-Type", "application/json"); + request.setOptions(restOptionsBuilder); + + Response response = client().performRequest(request); + return new JSONObject(getResponseBody(response)); + } +} diff --git a/integ-test/src/test/resources/indexDefinitions/text_for_cast_index_mappings.json b/integ-test/src/test/resources/indexDefinitions/text_for_cast_index_mappings.json new file mode 100644 index 0000000000..5e45eec6e7 --- /dev/null +++ b/integ-test/src/test/resources/indexDefinitions/text_for_cast_index_mappings.json @@ -0,0 +1,56 @@ +{ + "mappings" : { + "properties" : { + "TestName" : { + "type" : "keyword" + }, + "TextWithKeywords" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 25 + } + } + }, + "Text" : { + "type" : "text" + }, + "TextWithFielddata" : { + "type" : "text", + "fielddata" : true + }, + "TextWithFields" : { + "type" : "text", + "fields" : { + "words" : { + "type" : "keyword", + "ignore_above" : 25 + } + } + }, + "TextWithNumbers": { + "type": "text", + "fields": { + "values": { + "type": "integer" + } + } + }, + "TextWithMixedFields" : { + "type" : "text", + "fields" : { + "numeric": { + "type": "double", + "ignore_malformed": true + }, + "date": { + "type": "date", + "format": "yyyy-MM-dd", + "ignore_malformed": true + } + } + } + } + } +} diff --git a/integ-test/src/test/resources/indexDefinitions/text_index_mappings.json b/integ-test/src/test/resources/indexDefinitions/text_index_mappings.json new file mode 100644 index 0000000000..750ffa261c --- /dev/null +++ b/integ-test/src/test/resources/indexDefinitions/text_index_mappings.json @@ -0,0 +1,56 @@ +{ + "mappings" : { + "properties" : { + "Keyword" : { + "type" : "keyword" + }, + "TextWithKeywords" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 25 + } + } + }, + "Text" : { + "type" : "text" + }, + "TextWithFielddata" : { + "type" : "text", + "fielddata" : true + }, + "TextWithFields" : { + "type" : "text", + "fields" : { + "words" : { + "type" : "keyword", + "ignore_above" : 25 + } + } + }, + "TextWithNumbers": { + "type": "text", + "fields": { + "values": { + "type": "integer" + } + } + }, + "TextWithMixedFields" : { + "type" : "text", + "fields" : { + "numeric": { + "type": "double", + "ignore_malformed": true + }, + "date": { + "type": "date", + "format": "dd.MM.uuuu", + "ignore_malformed": true + } + } + } + } + } +} diff --git a/integ-test/src/test/resources/text.json b/integ-test/src/test/resources/text.json new file mode 100644 index 0000000000..20cb05b135 --- /dev/null +++ b/integ-test/src/test/resources/text.json @@ -0,0 +1,10 @@ +{"index": {}} +{ "Keyword" : "K for Keyword", "TextWithKeywords" : "Text With Keywords", "TextWithFielddata" : "Text", "TextWithFields" : null, "TextWithMixedFields": "one two tree" } +{"index": {}} +{ "Keyword" : null, "Text" : "Text", "TextWithFielddata" : "Fielddata is the best!", "TextWithFields" : "Fields for freedom", "TextWithNumbers" : 42, "TextWithMixedFields": 42 } +{"index": {}} +{ "Keyword" : "ikiki", "TextWithKeywords" : "Another Text With Similar Keywords", "TextWithFielddata" : null, "TextWithNumbers" : "42", "TextWithMixedFields": "12.04.1961" } +{"index": {}} +{ "TextWithKeywords" : "", "Text" : "Some text", "TextWithFielddata" : "Text With Fielddata", "TextWithFields" : "Freedom for fields", "TextWithNumbers" : 100500, "TextWithMixedFields": null } +{"index": {}} +{ "Keyword" : "kokoko", "TextWithKeywords": null, "Text" : "A fairy tale about little mermaid", "TextWithFields" : "A Text with a Field", "TextWithNumbers" : null } diff --git a/integ-test/src/test/resources/text_for_cast.json b/integ-test/src/test/resources/text_for_cast.json new file mode 100644 index 0000000000..a4caa621d6 --- /dev/null +++ b/integ-test/src/test/resources/text_for_cast.json @@ -0,0 +1,14 @@ +{"index": {}} +{ "TestName" : "bool", "Text" : "TRUE", "TextWithKeywords" : "true", "TextWithFielddata" : "TrUe", "TextWithFields" : "TRUE", "TextWithNumbers" : 42, "TextWithMixedFields": "trUE" } +{"index": {}} +{ "TestName" : "bool", "Text" : "FALSE", "TextWithKeywords" : "false", "TextWithFielddata" : "FaLSe", "TextWithFields" : "fAlsE", "TextWithNumbers" : 0, "TextWithMixedFields": 0 } +{"index": {}} +{ "TestName" : "int", "Text" : "0", "TextWithKeywords" : "1", "TextWithFielddata" : "-14", "TextWithFields" : "4096", "TextWithNumbers" : 100500, "TextWithMixedFields": 42 } +{"index": {}} +{ "TestName" : "double", "Text" : "0", "TextWithKeywords" : "1.", "TextWithFielddata" : "3.14", "TextWithFields" : ".223", "TextWithNumbers" : -333, "TextWithMixedFields": 42 } +{"index": {}} +{ "TestName" : "date", "Text" : "1961-04-12", "TextWithKeywords" : "1984-04-04", "TextWithFielddata" : "1999-10-20", "TextWithFields" : "2020-02-29", "TextWithNumbers" : "20111122", "TextWithMixedFields": "1977-07-17" } +{"index": {}} +{ "TestName" : "time", "Text" : "10:20:30", "TextWithKeywords" : "22:22", "TextWithFielddata" : "00:00:00", "TextWithFields" : "00:00:01", "TextWithNumbers" : "070809", "TextWithMixedFields": "07:17" } +{"index": {}} +{ "TestName" : "timestamp", "Text" : "1961-04-12 09:07:00", "TextWithKeywords" : "1984-04-04 10:20:30", "TextWithFielddata" : "1999-10-20 00:01:02", "TextWithFields" : "2020-02-29 00:00:00", "TextWithNumbers" : null, "TextWithMixedFields": "1977-07-17 01:02:03" } From ccf91386b3ab05ae9ce3f19504fb8c4a0b59c7fe Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 20 Jul 2023 20:34:48 -0700 Subject: [PATCH 05/12] Fix type resolution in IT. Signed-off-by: Yury-Fridlyand --- .../test/java/org/opensearch/sql/ppl/DataTypeIT.java | 8 ++++---- .../java/org/opensearch/sql/ppl/SystemFunctionIT.java | 11 +++++------ .../java/org/opensearch/sql/sql/ConditionalIT.java | 2 +- .../java/org/opensearch/sql/sql/SystemFunctionIT.java | 8 +++----- .../sql/opensearch/data/type/OpenSearchDataType.java | 2 +- .../data/value/OpenSearchExprValueFactory.java | 4 ++++ 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/DataTypeIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/DataTypeIT.java index 9911c35d8f..7499d8e843 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/DataTypeIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/DataTypeIT.java @@ -36,8 +36,8 @@ public void test_numeric_data_types() throws IOException { schema("byte_number", "byte"), schema("double_number", "double"), schema("float_number", "float"), - schema("half_float_number", "float"), - schema("scaled_float_number", "double")); + schema("half_float_number", "half_float"), + schema("scaled_float_number", "scaled_float")); } @Test @@ -51,8 +51,8 @@ public void test_nonnumeric_data_types() throws IOException { schema("binary_value", "binary"), schema("date_value", "timestamp"), schema("ip_value", "ip"), - schema("object_value", "struct"), - schema("nested_value", "array"), + schema("object_value", "object"), + schema("nested_value", "nested"), schema("geo_point_value", "geo_point")); } diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/SystemFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/SystemFunctionIT.java index de13aa5488..e9038d9826 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/SystemFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/SystemFunctionIT.java @@ -56,19 +56,18 @@ public void typeof_opensearch_types() throws IOException { + " | fields `double`, `long`, `integer`, `byte`, `short`, `float`, `half_float`, `scaled_float`", TEST_INDEX_DATATYPE_NUMERIC)); verifyDataRows(response, - rows("DOUBLE", "LONG", "INTEGER", "BYTE", "SHORT", "FLOAT", "FLOAT", "DOUBLE")); + rows("DOUBLE", "LONG", "INTEGER", "BYTE", "SHORT", "FLOAT", "HALF_FLOAT", "SCALED_FLOAT")); response = executeQuery(String.format("source=%s | eval " + "`text` = typeof(text_value), `date` = typeof(date_value)," + "`boolean` = typeof(boolean_value), `object` = typeof(object_value)," + "`keyword` = typeof(keyword_value), `ip` = typeof(ip_value)," - + "`binary` = typeof(binary_value), `geo_point` = typeof(geo_point_value)" - // TODO activate this test once `ARRAY` type supported, see ExpressionAnalyzer::isTypeNotSupported - //+ ", `nested` = typeof(nested_value)" - + " | fields `text`, `date`, `boolean`, `object`, `keyword`, `ip`, `binary`, `geo_point`", + + "`binary` = typeof(binary_value), `geo_point` = typeof(geo_point_value)," + + "`nested` = typeof(nested_value)" + + " | fields `text`, `date`, `boolean`, `object`, `keyword`, `ip`, `binary`, `geo_point`, `nested`", TEST_INDEX_DATATYPE_NONNUMERIC)); verifyDataRows(response, rows("TEXT", "TIMESTAMP", "BOOLEAN", "OBJECT", "KEYWORD", - "IP", "BINARY", "GEO_POINT")); + "IP", "BINARY", "GEO_POINT", "NESTED")); } } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java index 162ce1ae26..936612b611 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java @@ -60,7 +60,7 @@ public void ifnullWithNullInputTest() { + " WHERE balance is null limit 2", "jdbc")); verifySchema(response, - schema("IFNULL(null, firstname)", "IFNULL1", "text"), + schema("IFNULL(null, firstname)", "IFNULL1", "keyword"), schema("IFNULL(firstname, null)", "IFNULL2", "text"), schema("IFNULL(null, null)", "IFNULL3", "byte")); verifyDataRows(response, diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/SystemFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/SystemFunctionIT.java index 584cdd05dd..c0af48080c 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/SystemFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/SystemFunctionIT.java @@ -45,16 +45,14 @@ public void typeof_opensearch_types() { + "typeof(float_number), typeof(half_float_number), typeof(scaled_float_number)" + " from %s;", TEST_INDEX_DATATYPE_NUMERIC)); verifyDataRows(response, - rows("DOUBLE", "LONG", "INTEGER", "BYTE", "SHORT", "FLOAT", "FLOAT", "DOUBLE")); + rows("DOUBLE", "LONG", "INTEGER", "BYTE", "SHORT", "FLOAT", "HALF_FLOAT", "SCALED_FLOAT")); response = executeJdbcRequest(String.format("SELECT typeof(text_value)," + "typeof(date_value), typeof(boolean_value), typeof(object_value), typeof(keyword_value)," - + "typeof(ip_value), typeof(binary_value), typeof(geo_point_value)" - // TODO activate this test once `ARRAY` type supported, see ExpressionAnalyzer::isTypeNotSupported - //+ ", typeof(nested_value)" + + "typeof(ip_value), typeof(binary_value), typeof(geo_point_value), typeof(nested_value)" + " from %s;", TEST_INDEX_DATATYPE_NONNUMERIC)); verifyDataRows(response, rows("TEXT", "TIMESTAMP", "BOOLEAN", "OBJECT", "KEYWORD", - "IP", "BINARY", "GEO_POINT")); + "IP", "BINARY", "GEO_POINT", "NESTED")); } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java index 67929d76ce..c4fc5c5390 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java @@ -35,7 +35,7 @@ public boolean equals(final Object o) { } OpenSearchDataType other = (OpenSearchDataType) o; if (mappingType != null && other.mappingType != null) { - return mappingType.equals(other.mappingType); + return mappingType.equals(other.mappingType) && exprCoreType.equals(other.exprCoreType); } return exprCoreType.equals(other.exprCoreType); } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactory.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactory.java index 95815d5c38..670b001141 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactory.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactory.java @@ -119,6 +119,10 @@ public void extendTypeMapping(Map typeMapping) { (c, dt) -> new ExprFloatValue(c.floatValue())) .put(OpenSearchDataType.of(OpenSearchDataType.MappingType.Double), (c, dt) -> new ExprDoubleValue(c.doubleValue())) + .put(OpenSearchDataType.of(OpenSearchDataType.MappingType.HalfFloat), + (c, dt) -> new ExprFloatValue(c.floatValue())) + .put(OpenSearchDataType.of(OpenSearchDataType.MappingType.ScaledFloat), + (c, dt) -> new ExprDoubleValue(c.doubleValue())) .put(OpenSearchDataType.of(OpenSearchDataType.MappingType.Text), (c, dt) -> new OpenSearchExprTextValue(c.stringValue())) .put(OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword), From 0339320c5baccdb0652af36d8762579baa51a463 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 21 Jul 2023 11:53:03 -0700 Subject: [PATCH 06/12] Code clean up and comment update. Signed-off-by: Yury-Fridlyand --- .../opensearch/sql/data/type/ExprType.java | 10 +++++--- .../sql/data/type/WideningTypeRule.java | 15 ++++-------- docs/dev/text-type.md | 7 ++++++ .../data/type/OpenSearchDataType.java | 24 ++++++++++--------- .../data/type/OpenSearchDateType.java | 2 +- .../data/type/OpenSearchTextType.java | 8 ++----- .../opensearch/storage/OpenSearchIndex.java | 2 +- .../dsl/AggregationBuilderHelper.java | 1 - .../dsl/BucketAggregationBuilder.java | 1 - .../storage/script/core/ExpressionScript.java | 1 - .../script/filter/lucene/LikeQuery.java | 1 - .../script/filter/lucene/TermQuery.java | 2 -- .../storage/script/sort/SortQueryBuilder.java | 2 -- 13 files changed, 35 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java index 7dd3f1a669..a02b3e1712 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java +++ b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java @@ -50,7 +50,7 @@ default boolean shouldCast(ExprType other) { * Get the parent type. */ default List getParent() { - return Arrays.asList(UNKNOWN); + return List.of(UNKNOWN); } /** @@ -65,12 +65,16 @@ default String legacyTypeName() { return typeName(); } - // TODO doc + /** + * Perform field name conversion if needed before inserting it into a search query. + */ default String convertFieldForSearchQuery(String fieldName) { return fieldName; } - // TODO doc + /** + * Perform value conversion if needed before inserting it into a search query. + */ default Object convertValueForSearchQuery(ExprValue value) { return value.value(); } diff --git a/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java b/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java index 22144783bd..c472563aba 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java +++ b/core/src/main/java/org/opensearch/sql/data/type/WideningTypeRule.java @@ -13,15 +13,8 @@ /** * The definition of widening type rule for expression value. - * ExprType Widens to data types - * INTEGER LONG, FLOAT, DOUBLE - * LONG FLOAT, DOUBLE - * FLOAT DOUBLE - * DOUBLE DOUBLE - * STRING STRING - * BOOLEAN BOOLEAN - * ARRAY ARRAY - * STRUCT STRUCT + * See type widening definitions in {@link ExprCoreType}. + * For example, SHORT widens BYTE and so on. */ @UtilityClass public class WideningTypeRule { @@ -53,9 +46,9 @@ private static int distance(ExprType type1, ExprType type2, int distance) { } /** - * The max type among two types. The max is defined as follow + * The max type among two types. The max is defined as follows: * if type1 could widen to type2, then max is type2, vice versa - * if type1 could't widen to type2 and type2 could't widen to type1, + * if type1 couldn't widen to type2 and type2 couldn't widen to type1, * then throw {@link ExpressionEvaluationException}. * * @param type1 type1 diff --git a/docs/dev/text-type.md b/docs/dev/text-type.md index 9fc2033e2a..0c82d5c5a2 100644 --- a/docs/dev/text-type.md +++ b/docs/dev/text-type.md @@ -36,3 +36,10 @@ The solution is to provide to `:core` non simplified types, but full types. Thos 2. Update `OpenSearchDataType` (and inheritors if needed) to be comparable with `ExprCoreType`. 3. Update `:core` to do proper comparison (use `.equals` instead of `==`). 5. Update `:opensearch` to use the mapping information received from `:core` and properly build the search query. + +## Type Schema + +| JDBC type | `ExprCoreType` | `OpenSearchDataType` | OpenSearch type | +| --- | --- | --- | --- | +| `VARCHAR`/`CHAR` | `STRING` | -- | `keyword` | +| `LONGVARCHAR`/`TEXT` | `STRING` | `OpenSearchTextType` | `text` | diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java index c4fc5c5390..b84102a119 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java @@ -17,12 +17,17 @@ import org.apache.commons.lang3.EnumUtils; import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.data.type.ExprType; +import org.opensearch.sql.data.type.WideningTypeRule; /** * The extension of ExprType in OpenSearch. */ public class OpenSearchDataType implements ExprType, Serializable { + /** + * Redefine comparison operation: class (or derived) could be compared with ExprCoreType too. + * Used in {@link WideningTypeRule#distance(ExprType, ExprType)}. + */ public boolean equals(final Object o) { if (o == this) { return true; @@ -46,17 +51,17 @@ public int hashCode() { @Override public List getParent() { - return exprCoreType == ExprCoreType.UNKNOWN - ? List.of(ExprCoreType.UNKNOWN) - : exprCoreType.getParent(); + return exprCoreType.getParent(); } @Override public boolean shouldCast(ExprType other) { - ExprCoreType otherCoreType = other instanceof ExprCoreType ? (ExprCoreType) other - : (other instanceof OpenSearchDataType - ? ((OpenSearchDataType) other).exprCoreType : ExprCoreType.UNKNOWN); - // TODO Copied from BuiltinFunctionRepository.isCastRequired + ExprCoreType otherCoreType = ExprCoreType.UNKNOWN; + if (other instanceof ExprCoreType) { + otherCoreType = (ExprCoreType) other; + } else if (other instanceof OpenSearchDataType) { + otherCoreType = ((OpenSearchDataType) other).exprCoreType; + } if (ExprCoreType.numberTypes().contains(exprCoreType) && ExprCoreType.numberTypes().contains(otherCoreType)) { return false; @@ -163,9 +168,6 @@ public static Map parseMapping(Map i return; } // create OpenSearchDataType - - // TODO parse `fielddata` - result.put(k, OpenSearchDataType.of( EnumUtils.getEnumIgnoreCase(OpenSearchDataType.MappingType.class, type), innerMap) @@ -196,7 +198,7 @@ public static OpenSearchDataType of(MappingType mappingType, Map objectDataType.properties = properties; return objectDataType; case Text: - // TODO update these 2 below #1038 https://github.com/opensearch-project/sql/issues/1038 + // don't parse `fielddata`, because it does not contain any info which could be used by SQL Map fields = parseMapping((Map) innerMap.getOrDefault("fields", Map.of())); return (!fields.isEmpty()) ? OpenSearchTextType.of(fields) : OpenSearchTextType.of(); diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java index 384b9e6b88..360c273945 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java @@ -416,7 +416,7 @@ public String legacyTypeName() { @Override public Object convertValueForSearchQuery(ExprValue value) { - // TODO fix for https://github.com/opensearch-project/sql/issues/1847 + // TODO add here fix for https://github.com/opensearch-project/sql/issues/1847 return value.timestampValue().toEpochMilli(); } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java index 012b001245..b5a9ddb940 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java @@ -63,15 +63,11 @@ protected OpenSearchDataType cloneEmpty() { @Override public String convertFieldForSearchQuery(String fieldName) { - if (fields.size() > 1) { - // TODO or pick first? - throw new RuntimeException("too many text fields"); - } if (fields.size() == 0) { return fieldName; } - // TODO what if field is not a keyword - // https://github.com/opensearch-project/sql/issues/1112 + // Pick first field. What to do if there are multiple fields? + // Multi-field text support requested in https://github.com/opensearch-project/sql/issues/1887 return String.format("%s.%s", fieldName, fields.keySet().toArray()[0]); } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java index 70055e9dfc..f41e9dc732 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java @@ -127,7 +127,7 @@ public Map getFieldTypes() { cachedFieldTypes = OpenSearchDataType.traverseAndFlatten(cachedFieldOpenSearchTypes) .entrySet().stream().collect( LinkedHashMap::new, - (map, item) -> map.put(item.getKey(), item.getValue()/*.getExprType()*/), + (map, item) -> map.put(item.getKey(), item.getValue()), Map::putAll); } return cachedFieldTypes; diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java index a914ce757e..f93f5b37ca 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java @@ -17,7 +17,6 @@ import org.opensearch.sql.expression.FunctionExpression; import org.opensearch.sql.expression.LiteralExpression; import org.opensearch.sql.expression.ReferenceExpression; -import org.opensearch.sql.opensearch.data.type.OpenSearchTextType; import org.opensearch.sql.opensearch.storage.serialization.ExpressionSerializer; /** diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java index 8a2638ae85..44922c9b6c 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java @@ -13,7 +13,6 @@ import com.google.common.collect.ImmutableList; import java.util.List; import java.util.stream.Stream; - import org.apache.commons.lang3.tuple.Triple; import org.opensearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; import org.opensearch.search.aggregations.bucket.composite.DateHistogramValuesSourceBuilder; diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java index 4dd4a4862b..4a7537e287 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/core/ExpressionScript.java @@ -29,7 +29,6 @@ import org.opensearch.sql.expression.env.Environment; import org.opensearch.sql.expression.parse.ParseExpression; import org.opensearch.sql.opensearch.data.type.OpenSearchDataType; -import org.opensearch.sql.opensearch.data.type.OpenSearchTextType; import org.opensearch.sql.opensearch.data.value.OpenSearchExprValueFactory; /** diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java index 41a9a2d7f8..bb796bee9f 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/LikeQuery.java @@ -10,7 +10,6 @@ import org.opensearch.index.query.WildcardQueryBuilder; import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprType; -import org.opensearch.sql.opensearch.data.type.OpenSearchTextType; import org.opensearch.sql.opensearch.storage.script.StringUtils; public class LikeQuery extends LuceneQuery { diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java index 242402d30c..14fcbfbcc7 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/TermQuery.java @@ -9,9 +9,7 @@ import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.QueryBuilders; import org.opensearch.sql.data.model.ExprValue; -import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.data.type.ExprType; -import org.opensearch.sql.opensearch.data.type.OpenSearchTextType; /** * Lucene query that build term query for equality comparison. diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java index 22848036e1..bdee6bac96 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/sort/SortQueryBuilder.java @@ -20,8 +20,6 @@ import org.opensearch.sql.expression.Expression; import org.opensearch.sql.expression.FunctionExpression; import org.opensearch.sql.expression.ReferenceExpression; -import org.opensearch.sql.expression.function.BuiltinFunctionName; -import org.opensearch.sql.opensearch.data.type.OpenSearchTextType; /** * Builder of {@link SortBuilder}. From e885a442a697ab52b7ac50248df2cb0bb9e99001 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 25 Jul 2023 18:47:15 -0700 Subject: [PATCH 07/12] Minor clean up. Signed-off-by: Yury-Fridlyand --- .../sql/expression/operator/convert/TypeCastOperator.java | 1 - .../src/test/java/org/opensearch/sql/sql/ConditionalIT.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java b/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java index 6dac9c300a..d3295a53f0 100644 --- a/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java +++ b/core/src/main/java/org/opensearch/sql/expression/operator/convert/TypeCastOperator.java @@ -16,7 +16,6 @@ import static org.opensearch.sql.data.type.ExprCoreType.LONG; import static org.opensearch.sql.data.type.ExprCoreType.SHORT; import static org.opensearch.sql.data.type.ExprCoreType.STRING; -//import static org.opensearch.sql.data.type.ExprCoreType.TEXT; import static org.opensearch.sql.data.type.ExprCoreType.TIME; import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.opensearch.sql.expression.function.FunctionDSL.impl; diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java index 936612b611..9a833e0aa1 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java @@ -61,7 +61,7 @@ public void ifnullWithNullInputTest() { verifySchema(response, schema("IFNULL(null, firstname)", "IFNULL1", "keyword"), - schema("IFNULL(firstname, null)", "IFNULL2", "text"), + schema("IFNULL(firstname, null)", "IFNULL2", "keyword"), schema("IFNULL(null, null)", "IFNULL3", "byte")); verifyDataRows(response, rows("Hattie", "Hattie", LITERAL_NULL.value()), From 93e9a346cff3012308e08aae4a85c1be30097729 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 27 Jul 2023 10:12:32 -0700 Subject: [PATCH 08/12] Update `ExprType` and tests. Signed-off-by: Yury-Fridlyand --- .../opensearch/sql/data/type/ExprType.java | 4 ++-- .../sql/data/type/ExprTypeTest.java | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java index a02b3e1712..c9310a75ac 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java +++ b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java @@ -8,7 +8,6 @@ import static org.opensearch.sql.data.type.ExprCoreType.UNKNOWN; -import java.util.Arrays; import java.util.List; import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.expression.Expression; @@ -21,7 +20,8 @@ public interface ExprType { * Is compatible with other types. */ default boolean isCompatible(ExprType other) { - if (this.equals(other)) { + // Do double direction check with `equals`, because a derived class may override it + if (this.equals(other) || other.equals(this)) { return true; } else { if (other.equals(UNKNOWN)) { diff --git a/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java b/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java index 7db856d092..ad3a111006 100644 --- a/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java +++ b/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java @@ -28,6 +28,7 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import org.opensearch.sql.data.model.ExprDoubleValue; class ExprTypeTest { @Test @@ -49,6 +50,22 @@ public void isCompatible() { assertTrue(DATETIME.isCompatible(STRING)); } + @Test + public void isCompatibleTwoDirectionCheck() { + ExprType other = new ExprType() { + @Override + public String typeName() { + return null; + } + + @Override + public boolean equals(Object obj) { + return true; + } + }; + assertTrue(UNDEFINED.isCompatible(other)); + } + @Test public void isNotCompatible() { assertFalse(INTEGER.isCompatible(DOUBLE)); @@ -88,4 +105,11 @@ void defaultLegacyTypeName() { final ExprType exprType = () -> "dummy"; assertEquals("dummy", exprType.legacyTypeName()); } + + @Test + void defaultConvert() { + ExprType exprType = () -> null; + assertEquals("field", exprType.convertFieldForSearchQuery("field")); + assertEquals(3.14, exprType.convertValueForSearchQuery(new ExprDoubleValue(3.14))); + } } From d3b66ac9da5fefeee90541e06cbd9de408d0738c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 27 Jul 2023 10:12:51 -0700 Subject: [PATCH 09/12] Minor fix in docs. Signed-off-by: Yury-Fridlyand --- docs/dev/query-type-conversion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/query-type-conversion.md b/docs/dev/query-type-conversion.md index 948b47b681..917fb414a9 100644 --- a/docs/dev/query-type-conversion.md +++ b/docs/dev/query-type-conversion.md @@ -31,7 +31,7 @@ However, more general conversions for non-numeric types are missing, such as con The common use case and motivation include: 1. *User-friendly*: Although it doesn't matter for application or BI tool which can always follow the strict grammar rule, it's more friendly and accessible to human by implicit type conversion, ex. `date > DATE('2020-06-01') => date > '2020-06-01'` -2. *Schema-on-read*: More importantly, implicit conversion from string is required for schema on read (stored as raw string on write and extract field(s) on read), ex. `regex ‘...' | abs(a)` +2. *Schema-on-read*: More importantly, implicit conversion from string is required for schema on read (stored as raw string on write and extract field(s) on read), ex. `regex '...' | abs(a)` ### 2.2 Functionalities From 5232ad241911135a7e5b852d2b11eb70f0d5fb83 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 27 Jul 2023 10:14:53 -0700 Subject: [PATCH 10/12] Finalize implementation and tests. Signed-off-by: Yury-Fridlyand --- .../data/type/OpenSearchDataType.java | 7 +- .../data/type/OpenSearchDateType.java | 21 ++- .../data/type/OpenSearchTextType.java | 14 +- .../client/OpenSearchNodeClientTest.java | 2 +- .../client/OpenSearchRestClientTest.java | 2 +- .../data/type/OpenSearchDataTypeTest.java | 114 +++++++++++----- .../data/type/OpenSearchDateTypeTest.java | 35 ++++- .../data/type/OpenSearchTextTypeTest.java | 107 +++++++++++++++ .../value/OpenSearchExprTextValueTest.java | 54 +++----- .../value/OpenSearchExprValueFactoryTest.java | 29 ++++ .../AggregationQueryBuilderTest.java | 124 +++++++++--------- .../ExpressionAggregationScriptTest.java | 2 +- .../dsl/BucketAggregationBuilderTest.java | 18 +-- .../filter/ExpressionFilterScriptTest.java | 2 +- .../script/filter/FilterQueryBuilderTest.java | 14 +- 15 files changed, 356 insertions(+), 189 deletions(-) create mode 100644 opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java index b84102a119..17c7498c5d 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java @@ -66,7 +66,7 @@ public boolean shouldCast(ExprType other) { && ExprCoreType.numberTypes().contains(otherCoreType)) { return false; } - return exprCoreType == ExprCoreType.UNKNOWN || exprCoreType.shouldCast(other); + return exprCoreType == ExprCoreType.UNKNOWN || exprCoreType.shouldCast(otherCoreType); } /** @@ -181,6 +181,7 @@ public static Map parseMapping(Map i * @param mappingType A mapping type. * @return An instance or inheritor of `OpenSearchDataType`. */ + @SuppressWarnings("unchecked") public static OpenSearchDataType of(MappingType mappingType, Map innerMap) { OpenSearchDataType res = instances.getOrDefault(mappingType.toString(), new OpenSearchDataType(mappingType) @@ -207,8 +208,8 @@ public static OpenSearchDataType of(MappingType mappingType, Map case Ip: return OpenSearchIpType.of(); case Date: // Default date formatter is used when "" is passed as the second parameter - String format = (String) innerMap.getOrDefault("format", ""); - return OpenSearchDateType.of(format); + return innerMap.isEmpty() ? OpenSearchDateType.of() + : OpenSearchDateType.of((String) innerMap.getOrDefault("format", "")); default: return res; } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java index 360c273945..ceb6e05238 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java @@ -149,11 +149,6 @@ private OpenSearchDateType(ExprCoreType exprCoreType) { this.exprCoreType = exprCoreType; } - private OpenSearchDateType(ExprType exprType) { - this(); - this.exprCoreType = (ExprCoreType) exprType; - } - private OpenSearchDateType(String format) { super(MappingType.Date); this.formats = getFormatList(format); @@ -374,25 +369,29 @@ public static OpenSearchDateType of(String format) { return new OpenSearchDateType(format); } + /** A public constructor replacement. */ public static OpenSearchDateType of(ExprCoreType exprCoreType) { + if (!isDateTypeCompatible(exprCoreType)) { + throw new IllegalArgumentException(String.format("Not a date/time type: %s", exprCoreType)); + } return new OpenSearchDateType(exprCoreType); } + /** A public constructor replacement. */ public static OpenSearchDateType of(ExprType exprType) { - return new OpenSearchDateType(exprType); + if (!isDateTypeCompatible(exprType)) { + throw new IllegalArgumentException(String.format("Not a date/time type: %s", exprType)); + } + return new OpenSearchDateType((ExprCoreType) exprType); } public static OpenSearchDateType of() { return OpenSearchDateType.instance; } - @Override - public List getParent() { - return List.of(exprCoreType); - } - @Override public boolean shouldCast(ExprType other) { + // TODO override to fix for https://github.com/opensearch-project/sql/issues/1847 return false; } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java index b5a9ddb940..d50ff431c8 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java @@ -8,10 +8,8 @@ import static org.opensearch.sql.data.type.ExprCoreType.STRING; import com.google.common.collect.ImmutableMap; -import java.util.List; import java.util.Map; import lombok.Getter; -import org.opensearch.sql.data.type.ExprType; /** * The type of a text value. See @@ -46,19 +44,9 @@ public static OpenSearchTextType of() { return OpenSearchTextType.instance; } - @Override - public List getParent() { - return List.of(STRING); - } - - @Override - public boolean shouldCast(ExprType other) { - return false; - } - @Override protected OpenSearchDataType cloneEmpty() { - return OpenSearchTextType.of(Map.copyOf(this.fields)); + return OpenSearchTextType.of(Map.copyOf(fields)); } @Override diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchNodeClientTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchNodeClientTest.java index 9417a1de1d..259de8a4d7 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchNodeClientTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchNodeClientTest.java @@ -184,7 +184,7 @@ void get_index_mappings() throws IOException { () -> assertEquals("KEYWORD", mapping.get("city").legacyTypeName()), () -> assertEquals(OpenSearchTextType.of(MappingType.Keyword), parsedTypes.get("city")), - () -> assertEquals("DATE", mapping.get("birthday").legacyTypeName()), + () -> assertEquals("TIMESTAMP", mapping.get("birthday").legacyTypeName()), () -> assertEquals(OpenSearchTextType.of(MappingType.Date), parsedTypes.get("birthday")), () -> assertEquals("GEO_POINT", mapping.get("location").legacyTypeName()), diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchRestClientTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchRestClientTest.java index cceb6de995..a4af37fec6 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchRestClientTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/client/OpenSearchRestClientTest.java @@ -185,7 +185,7 @@ void get_index_mappings() throws IOException { () -> assertEquals("KEYWORD", mapping.get("city").legacyTypeName()), () -> assertEquals(OpenSearchTextType.of(MappingType.Keyword), parsedTypes.get("city")), - () -> assertEquals("DATE", mapping.get("birthday").legacyTypeName()), + () -> assertEquals("TIMESTAMP", mapping.get("birthday").legacyTypeName()), () -> assertEquals(OpenSearchTextType.of(MappingType.Date), parsedTypes.get("birthday")), () -> assertEquals("GEO_POINT", mapping.get("location").legacyTypeName()), diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java index 8d69b3d855..3fcccaa832 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; @@ -19,7 +20,7 @@ import static org.opensearch.sql.data.type.ExprCoreType.ARRAY; import static org.opensearch.sql.data.type.ExprCoreType.BOOLEAN; import static org.opensearch.sql.data.type.ExprCoreType.BYTE; -import static org.opensearch.sql.data.type.ExprCoreType.DATE; +import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE; import static org.opensearch.sql.data.type.ExprCoreType.FLOAT; import static org.opensearch.sql.data.type.ExprCoreType.INTEGER; @@ -55,41 +56,96 @@ class OpenSearchDataTypeTest { private static final OpenSearchDateType dateType = OpenSearchDateType.of(emptyFormatString); + public static class TestType extends OpenSearchDataType { + + public TestType(MappingType mappingType, ExprCoreType type) { + super(mappingType); + exprCoreType = type; + } + } + + @Test + public void equals() { + assertAll( + () -> assertEquals(textType, textType), + () -> assertEquals(OpenSearchDataType.of(INTEGER), INTEGER), + () -> assertNotEquals(textType, 42), + () -> assertEquals(OpenSearchDataType.of(MappingType.GeoPoint), + OpenSearchDataType.of(MappingType.GeoPoint)), + () -> assertNotEquals(OpenSearchDataType.of(MappingType.GeoPoint), + OpenSearchDataType.of(MappingType.Ip)), + () -> assertEquals(OpenSearchDataType.of(STRING), OpenSearchDataType.of(STRING)), + () -> assertEquals(OpenSearchDataType.of(DOUBLE), + OpenSearchDataType.of(MappingType.Double)), + () -> assertEquals(OpenSearchDataType.of(MappingType.Double), + OpenSearchDataType.of(DOUBLE)), + () -> assertEquals(OpenSearchDataType.of(DOUBLE), OpenSearchDataType.of(DOUBLE)), + () -> assertEquals(new TestType(MappingType.Double, DOUBLE), + new TestType(MappingType.Double, DOUBLE)), + () -> assertNotEquals(new TestType(MappingType.Ip, UNKNOWN), + new TestType(MappingType.Binary, UNKNOWN)), + () -> assertNotEquals(OpenSearchDataType.of(MappingType.Date), + OpenSearchDateType.of("date")), + () -> assertNotEquals(OpenSearchDataType.of(INTEGER), OpenSearchDataType.of(DOUBLE)) + ); + } + + @Test + public void getParent() { + assertEquals(DATETIME.getParent(), OpenSearchDataType.of(DATETIME).getParent()); + } + @Test public void isCompatible() { - assertTrue(STRING.isCompatible(textType)); - assertFalse(textType.isCompatible(STRING)); + assertAll( + () -> assertTrue(STRING.isCompatible(textType)), + () -> assertTrue(textType.isCompatible(STRING)), - assertTrue(STRING.isCompatible(textKeywordType)); - assertTrue(textType.isCompatible(textKeywordType)); + () -> assertTrue(STRING.isCompatible(textKeywordType)), + () -> assertTrue(textType.isCompatible(textKeywordType)) + ); } // `typeName` and `legacyTypeName` return different things: // https://github.com/opensearch-project/sql/issues/1296 @Test public void typeName() { - assertEquals("STRING", textType.typeName()); - assertEquals("STRING", textKeywordType.typeName()); - assertEquals("OBJECT", OpenSearchDataType.of(MappingType.Object).typeName()); - assertEquals("DATE", OpenSearchDataType.of(MappingType.Date).typeName()); - assertEquals("DOUBLE", OpenSearchDataType.of(MappingType.Double).typeName()); - assertEquals("KEYWORD", OpenSearchDataType.of(MappingType.Keyword).typeName()); + assertAll( + () -> assertEquals("STRING", textType.typeName()), + () -> assertEquals("STRING", textKeywordType.typeName()), + () -> assertEquals("OBJECT", OpenSearchDataType.of(MappingType.Object).typeName()), + () -> assertEquals("TIMESTAMP", OpenSearchDataType.of(MappingType.Date).typeName()), + () -> assertEquals("DOUBLE", OpenSearchDataType.of(MappingType.Double).typeName()), + () -> assertEquals("STRING", OpenSearchDataType.of(MappingType.Keyword).typeName()) + ); } @Test public void legacyTypeName() { - assertEquals("TEXT", textType.legacyTypeName()); - assertEquals("TEXT", textKeywordType.legacyTypeName()); - assertEquals("OBJECT", OpenSearchDataType.of(MappingType.Object).legacyTypeName()); - assertEquals("DATE", OpenSearchDataType.of(MappingType.Date).legacyTypeName()); - assertEquals("DOUBLE", OpenSearchDataType.of(MappingType.Double).legacyTypeName()); - assertEquals("KEYWORD", OpenSearchDataType.of(MappingType.Keyword).legacyTypeName()); + assertAll( + () -> assertEquals("TEXT", textType.legacyTypeName()), + () -> assertEquals("TEXT", textKeywordType.legacyTypeName()), + () -> assertEquals("OBJECT", OpenSearchDataType.of(MappingType.Object).legacyTypeName()), + () -> assertEquals("TIMESTAMP", OpenSearchDataType.of(MappingType.Date).legacyTypeName()), + () -> assertEquals("DOUBLE", OpenSearchDataType.of(MappingType.Double).legacyTypeName()), + () -> assertEquals("KEYWORD", OpenSearchDataType.of(MappingType.Keyword).legacyTypeName()) + ); } @Test public void shouldCast() { - assertFalse(textType.shouldCast(STRING)); - assertFalse(textKeywordType.shouldCast(STRING)); + assertAll( + () -> assertFalse(textType.shouldCast(STRING)), + () -> assertTrue(textType.shouldCast(INTEGER)), + () -> assertFalse(textKeywordType.shouldCast(STRING)), + () -> assertTrue(textType.shouldCast(() -> null)), + () -> assertFalse(OpenSearchDataType.of(MappingType.Keyword).shouldCast(STRING)), + () -> assertFalse(OpenSearchDataType.of(MappingType.Keyword).shouldCast(textType)), + () -> assertFalse(OpenSearchDataType.of(MappingType.Long).shouldCast(LONG)), + () -> assertTrue(OpenSearchDataType.of(MappingType.Long).shouldCast(STRING)), + () -> assertTrue(OpenSearchBinaryType.of().shouldCast(OpenSearchBinaryType.of())), + () -> assertTrue(OpenSearchBinaryType.of().shouldCast(STRING)) + ); } private static Stream getTestDataWithType() { @@ -105,15 +161,12 @@ private static Stream getTestDataWithType() { Arguments.of(MappingType.ScaledFloat, "scaled_float", DOUBLE), Arguments.of(MappingType.Double, "double", DOUBLE), Arguments.of(MappingType.Boolean, "boolean", BOOLEAN), - Arguments.of(MappingType.Date, "date", TIMESTAMP), + Arguments.of(MappingType.Date, "timestamp", TIMESTAMP), Arguments.of(MappingType.Object, "object", STRUCT), Arguments.of(MappingType.Nested, "nested", ARRAY), - Arguments.of(MappingType.GeoPoint, "geo_point", - OpenSearchGeoPointType.of()), - Arguments.of(MappingType.Binary, "binary", - OpenSearchBinaryType.of()), - Arguments.of(MappingType.Ip, "ip", - OpenSearchIpType.of()) + Arguments.of(MappingType.GeoPoint, "geo_point", OpenSearchGeoPointType.of()), + Arguments.of(MappingType.Binary, "binary", OpenSearchBinaryType.of()), + Arguments.of(MappingType.Ip, "ip", OpenSearchIpType.of()) ); } @@ -124,7 +177,7 @@ public void of_MappingType(MappingType mappingType, String name, ExprType dataTy // For serialization of SQL and PPL different functions are used, and it was designed to return // different types. No clue why, but it should be fixed in #1296. var nameForSQL = name.toUpperCase(); - var nameForPPL = name.equals("text") ? "STRING" : name.toUpperCase(); + var nameForPPL = name.equals("text") || name.equals("keyword") ? "STRING" : name.toUpperCase(); assertAll( () -> assertEquals(nameForPPL, type.typeName()), () -> assertEquals(nameForSQL, type.legacyTypeName()), @@ -396,7 +449,7 @@ private Map getSampleMapping() { } @Test - public void test_getExprType() { + public void getExprType() { assertEquals(OpenSearchTextType.of(), OpenSearchDataType.of(MappingType.Text).getExprType()); assertEquals(FLOAT, OpenSearchDataType.of(MappingType.Float).getExprType()); @@ -405,9 +458,4 @@ public void test_getExprType() { assertEquals(DOUBLE, OpenSearchDataType.of(MappingType.ScaledFloat).getExprType()); assertEquals(TIMESTAMP, OpenSearchDataType.of(MappingType.Date).getExprType()); } - - @Test - public void test_shouldCastFunction() { - assertFalse(dateType.shouldCast(DATE)); - } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateTypeTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateTypeTest.java index 13393da732..c219a3a6d3 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateTypeTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateTypeTest.java @@ -9,10 +9,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.opensearch.sql.data.type.ExprCoreType.DATE; import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; +import static org.opensearch.sql.data.type.ExprCoreType.STRING; import static org.opensearch.sql.data.type.ExprCoreType.TIME; import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.opensearch.sql.opensearch.data.type.OpenSearchDateType.SUPPORTED_NAMED_DATETIME_FORMATS; @@ -23,6 +25,7 @@ import static org.opensearch.sql.opensearch.data.type.OpenSearchDateType.isDateTypeCompatible; import com.google.common.collect.Lists; +import java.time.Instant; import java.util.EnumSet; import java.util.List; import java.util.stream.Stream; @@ -33,6 +36,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opensearch.common.time.FormatNames; +import org.opensearch.sql.data.model.ExprTimestampValue; +import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.type.ExprCoreType; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @@ -89,10 +94,10 @@ public void isCompatible() { public void check_typeName() { assertAll( // always use the MappingType of "DATE" - () -> assertEquals("DATE", defaultDateType.typeName()), - () -> assertEquals("DATE", timeDateType.typeName()), + () -> assertEquals("TIMESTAMP", defaultDateType.typeName()), + () -> assertEquals("TIME", timeDateType.typeName()), () -> assertEquals("DATE", dateDateType.typeName()), - () -> assertEquals("DATE", datetimeDateType.typeName()) + () -> assertEquals("TIMESTAMP", datetimeDateType.typeName()) ); } @@ -100,10 +105,10 @@ public void check_typeName() { public void check_legacyTypeName() { assertAll( // always use the legacy "DATE" type - () -> assertEquals("DATE", defaultDateType.legacyTypeName()), - () -> assertEquals("DATE", timeDateType.legacyTypeName()), + () -> assertEquals("TIMESTAMP", defaultDateType.legacyTypeName()), + () -> assertEquals("TIME", timeDateType.legacyTypeName()), () -> assertEquals("DATE", dateDateType.legacyTypeName()), - () -> assertEquals("DATE", datetimeDateType.legacyTypeName()) + () -> assertEquals("TIMESTAMP", datetimeDateType.legacyTypeName()) ); } @@ -261,4 +266,22 @@ public void check_if_date_type_compatible() { assertFalse(isDateTypeCompatible(OpenSearchDataType.of( OpenSearchDataType.MappingType.Text))); } + + @Test + public void throw_if_create_with_incompatible_type() { + assertThrows(IllegalArgumentException.class, () -> OpenSearchDateType.of(STRING)); + assertThrows(IllegalArgumentException.class, + () -> OpenSearchDateType.of(OpenSearchTextType.of())); + } + + @Test + public void convert_value() { + ExprValue value = new ExprTimestampValue(Instant.ofEpochMilli(42)); + assertEquals(42L, OpenSearchDateType.of().convertValueForSearchQuery(value)); + } + + @Test + public void shouldCast() { + assertFalse(OpenSearchDateType.of().shouldCast(() -> null)); + } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java new file mode 100644 index 0000000000..9350a51be4 --- /dev/null +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.opensearch.data.type; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opensearch.sql.data.type.ExprCoreType.DATE; +import static org.opensearch.sql.data.type.ExprCoreType.STRING; +import static org.opensearch.sql.opensearch.data.type.OpenSearchDataType.MappingType; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public class OpenSearchTextTypeTest { + + @Test + public void of() { + assertSame(OpenSearchTextType.of(), OpenSearchTextType.of()); + } + + @Test + public void fields_is_readonly() { + assertThrows(Throwable.class, () -> OpenSearchTextType.of().getFields() + .put("1", OpenSearchDataType.of(MappingType.Keyword))); + } + + @Test + public void of_map() { + var fields = Map.of("1", OpenSearchDataType.of(MappingType.Keyword)); + assertAll( + () -> assertNotSame(OpenSearchTextType.of(), OpenSearchTextType.of(Map.of())), + () -> assertEquals(fields, OpenSearchTextType.of(fields).getFields()) + ); + } + + @Test + public void expr_type() { + assertAll( + () -> assertEquals(STRING, OpenSearchTextType.of().getExprType()), + () -> assertEquals(MappingType.Text, OpenSearchTextType.of().getMappingType()) + ); + } + + @Test + public void cloneEmpty() { + var fields = Map.of("1", OpenSearchDataType.of(MappingType.Keyword)); + var textType = OpenSearchTextType.of(fields); + var clone = textType.cloneEmpty(); + assertAll( + // `equals` falls back to `OpenSearchDataType`, which ignores `fields` + () -> assertEquals(textType, clone), + () -> assertTrue(clone instanceof OpenSearchTextType), + () -> assertEquals(textType.getFields(), ((OpenSearchTextType) clone).getFields()) + ); + } + + @Test + public void getParent() { + assertEquals(STRING.getParent(), OpenSearchTextType.of().getParent()); + } + + @Test + public void shouldCast() { + assertAll( + () -> assertFalse(OpenSearchTextType.of().shouldCast(STRING)), + () -> assertTrue(OpenSearchTextType.of().shouldCast(DATE)), + () -> assertTrue(OpenSearchTextType.of() + .shouldCast(OpenSearchDataType.of(MappingType.Integer))), + () -> assertFalse(OpenSearchTextType.of() + .shouldCast(OpenSearchDataType.of(MappingType.Keyword))) + ); + } + + @Test + public void convertFieldForSearchQuery() { + var fieldsWithKeyword = Map.of("words", OpenSearchDataType.of(MappingType.Keyword)); + var fieldsWithNumber = Map.of("numbers", OpenSearchDataType.of(MappingType.Integer)); + // use ImmutableMap to avoid entry reordering + var fieldsWithMixed = ImmutableMap.of( + "numbers", OpenSearchDataType.of(MappingType.Integer), + "words", OpenSearchDataType.of(MappingType.Keyword)); + assertAll( + () -> assertEquals("field", OpenSearchTextType.of().convertFieldForSearchQuery("field")), + () -> assertEquals("field", + OpenSearchTextType.of(Map.of()).convertFieldForSearchQuery("field")), + () -> assertEquals("field.words", + OpenSearchTextType.of(fieldsWithKeyword).convertFieldForSearchQuery("field")), + () -> assertEquals("field.numbers", + OpenSearchTextType.of(fieldsWithNumber).convertFieldForSearchQuery("field")), + () -> assertEquals("field.numbers", + OpenSearchTextType.of(fieldsWithMixed).convertFieldForSearchQuery("field")) + ); + } +} diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprTextValueTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprTextValueTest.java index b60402e746..2eeef760d3 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprTextValueTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprTextValueTest.java @@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.opensearch.sql.data.type.ExprCoreType.INTEGER; -import static org.opensearch.sql.data.type.ExprCoreType.STRING; import java.util.Map; import org.junit.jupiter.api.DisplayNameGeneration; @@ -34,47 +32,27 @@ public void getFields() { assertEquals(fields, OpenSearchTextType.of(fields).getFields()); } - @Test - void non_text_types_arent_converted() { - assertAll( - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", - OpenSearchDataType.of(INTEGER))), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", - OpenSearchDataType.of(STRING))), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", - OpenSearchDataType.of(OpenSearchDataType.MappingType.GeoPoint))), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", - OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword))), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", - OpenSearchDataType.of(OpenSearchDataType.MappingType.Integer))), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", STRING)), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", INTEGER)) - ); - } - - @Test - void non_text_types_with_nested_objects_arent_converted() { - var objectType = OpenSearchDataType.of(OpenSearchDataType.MappingType.Object, - Map.of("subfield", OpenSearchDataType.of(STRING))); - var arrayType = OpenSearchDataType.of(OpenSearchDataType.MappingType.Nested, - Map.of("subfield", OpenSearchDataType.of(STRING))); - assertAll( - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", objectType)), - () -> assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", arrayType)) - ); - } - @Test void text_type_without_fields_isnt_converted() { - assertEquals("field", OpenSearchTextType.convertTextToKeyword("field", - OpenSearchDataType.of(OpenSearchDataType.MappingType.Text))); + assertEquals("field", OpenSearchDataType.of(OpenSearchDataType.MappingType.Text) + .convertFieldForSearchQuery("field")); } @Test void text_type_with_fields_is_converted() { - var textWithKeywordType = OpenSearchTextType.of(Map.of("keyword", - OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword))); - assertEquals("field.keyword", - OpenSearchTextType.convertTextToKeyword("field", textWithKeywordType)); + assertAll( + () -> assertEquals("field.keyword", + OpenSearchTextType.of(Map.of("keyword", + OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword))) + .convertFieldForSearchQuery("field")), + () -> assertEquals("field.words", + OpenSearchTextType.of(Map.of("words", + OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword))) + .convertFieldForSearchQuery("field")), + () -> assertEquals("field.numbers", + OpenSearchTextType.of(Map.of("numbers", + OpenSearchDataType.of(OpenSearchDataType.MappingType.Integer))) + .convertFieldForSearchQuery("field")) + ); } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactoryTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactoryTest.java index 827606a961..ec89a7592e 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactoryTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactoryTest.java @@ -71,7 +71,10 @@ class OpenSearchExprValueFactoryTest { .put("longV", OpenSearchDataType.of(LONG)) .put("floatV", OpenSearchDataType.of(FLOAT)) .put("doubleV", OpenSearchDataType.of(DOUBLE)) + .put("halfFloatV", OpenSearchDataType.of(OpenSearchDataType.MappingType.HalfFloat)) + .put("scaledFloatV", OpenSearchDataType.of(OpenSearchDataType.MappingType.ScaledFloat)) .put("stringV", OpenSearchDataType.of(STRING)) + .put("keywordV", OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword)) .put("dateV", OpenSearchDateType.of(DATE)) .put("datetimeV", OpenSearchDateType.of(DATETIME)) .put("timeV", OpenSearchDateType.of(TIME)) @@ -212,6 +215,23 @@ public void constructDouble() { ); } + @Test + public void constructHalfFloat() { + assertAll( + () -> assertEquals(floatValue(1f), tupleValue("{\"halfFloatV\":1.0}").get("halfFloatV")), + () -> assertEquals(floatValue(1f), constructFromObject("halfFloatV", 1f)) + ); + } + + @Test + public void constructScaledFloat() { + assertAll( + () -> assertEquals(doubleValue(1d), + tupleValue("{\"scaledFloatV\":1.0}").get("scaledFloatV")), + () -> assertEquals(doubleValue(1d), constructFromObject("scaledFloatV", 1d)) + ); + } + @Test public void constructString() { assertAll( @@ -221,6 +241,15 @@ public void constructString() { ); } + @Test + public void constructKeyword() { + assertAll( + () -> assertEquals(stringValue("text"), + tupleValue("{\"keywordV\":\"text\"}").get("keywordV")), + () -> assertEquals(stringValue("text"), constructFromObject("keywordV", "text")) + ); + } + @Test public void constructBoolean() { assertAll( diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/AggregationQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/AggregationQueryBuilderTest.java index 03f5cc8b52..915d2d72f5 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/AggregationQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/AggregationQueryBuilderTest.java @@ -31,7 +31,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.AbstractMap; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -100,9 +99,9 @@ void should_build_composite_aggregation_for_field_reference() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), - Arrays.asList(named("name", ref("name", STRING))))); + List.of( + named("avg(age)", new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER))), + List.of(named("name", ref("name", STRING))))); } @Test @@ -133,9 +132,9 @@ void should_build_composite_aggregation_for_field_reference_with_order() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), - Arrays.asList(named("name", ref("name", STRING))), + List.of( + named("avg(age)", new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER))), + List.of(named("name", ref("name", STRING))), sort(ref("name", STRING), Sort.SortOption.DEFAULT_DESC) )); } @@ -143,9 +142,9 @@ void should_build_composite_aggregation_for_field_reference_with_order() { @Test void should_build_type_mapping_for_field_reference() { assertThat( - buildTypeMapping(Arrays.asList( - named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), - Arrays.asList(named("name", ref("name", STRING)))), + buildTypeMapping(List.of( + named("avg(age)", new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER))), + List.of(named("name", ref("name", STRING)))), containsInAnyOrder( map("avg(age)", OpenSearchDataType.of(INTEGER)), map("name", OpenSearchDataType.of(STRING)) @@ -155,10 +154,10 @@ void should_build_type_mapping_for_field_reference() { @Test void should_build_type_mapping_for_datetime_type() { assertThat( - buildTypeMapping(Arrays.asList( + buildTypeMapping(List.of( named("avg(datetime)", - new AvgAggregator(Arrays.asList(ref("datetime", DATETIME)), DATETIME))), - Arrays.asList(named("datetime", ref("datetime", DATETIME)))), + new AvgAggregator(List.of(ref("datetime", DATETIME)), DATETIME))), + List.of(named("datetime", ref("datetime", DATETIME)))), containsInAnyOrder( map("avg(datetime)", OpenSearchDateType.of(DATETIME)), map("datetime", OpenSearchDateType.of(DATETIME)) @@ -168,10 +167,10 @@ void should_build_type_mapping_for_datetime_type() { @Test void should_build_type_mapping_for_timestamp_type() { assertThat( - buildTypeMapping(Arrays.asList( + buildTypeMapping(List.of( named("avg(timestamp)", - new AvgAggregator(Arrays.asList(ref("timestamp", TIMESTAMP)), TIMESTAMP))), - Arrays.asList(named("timestamp", ref("timestamp", TIMESTAMP)))), + new AvgAggregator(List.of(ref("timestamp", TIMESTAMP)), TIMESTAMP))), + List.of(named("timestamp", ref("timestamp", TIMESTAMP)))), containsInAnyOrder( map("avg(timestamp)", OpenSearchDateType.of()), map("timestamp", OpenSearchDateType.of()) @@ -181,10 +180,10 @@ void should_build_type_mapping_for_timestamp_type() { @Test void should_build_type_mapping_for_date_type() { assertThat( - buildTypeMapping(Arrays.asList( + buildTypeMapping(List.of( named("avg(date)", - new AvgAggregator(Arrays.asList(ref("date", DATE)), DATE))), - Arrays.asList(named("date", ref("date", DATE)))), + new AvgAggregator(List.of(ref("date", DATE)), DATE))), + List.of(named("date", ref("date", DATE)))), containsInAnyOrder( map("avg(date)", OpenSearchDateType.of(DATE)), map("date", OpenSearchDateType.of(DATE)) @@ -194,10 +193,10 @@ void should_build_type_mapping_for_date_type() { @Test void should_build_type_mapping_for_time_type() { assertThat( - buildTypeMapping(Arrays.asList( + buildTypeMapping(List.of( named("avg(time)", - new AvgAggregator(Arrays.asList(ref("time", TIME)), TIME))), - Arrays.asList(named("time", ref("time", TIME)))), + new AvgAggregator(List.of(ref("time", TIME)), TIME))), + List.of(named("time", ref("time", TIME)))), containsInAnyOrder( map("avg(time)", OpenSearchDateType.of(TIME)), map("time", OpenSearchDateType.of(TIME)) @@ -214,7 +213,7 @@ void should_build_composite_aggregation_for_field_reference_of_keyword() { + " \"sources\" : [ {%n" + " \"name\" : {%n" + " \"terms\" : {%n" - + " \"field\" : \"name.keyword\",%n" + + " \"field\" : \"name.words\",%n" + " \"missing_bucket\" : true,%n" + " \"missing_order\" : \"first\",%n" + " \"order\" : \"asc\"%n" @@ -232,18 +231,17 @@ void should_build_composite_aggregation_for_field_reference_of_keyword() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), - Arrays.asList(named("name", ref("name", OpenSearchTextType.of(Map.of("words", + List.of(named("avg(age)", new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER))), + List.of(named("name", ref("name", OpenSearchTextType.of(Map.of("words", OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword)))))))); } @Test void should_build_type_mapping_for_field_reference_of_keyword() { assertThat( - buildTypeMapping(Arrays.asList( - named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), - Arrays.asList(named("name", ref("name", STRING)))), + buildTypeMapping(List.of( + named("avg(age)", new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER))), + List.of(named("name", ref("name", STRING)))), containsInAnyOrder( map("avg(age)", OpenSearchDataType.of(INTEGER)), map("name", OpenSearchDataType.of(STRING)) @@ -288,10 +286,10 @@ void should_build_composite_aggregation_for_expression() { + " }%n" + "}"), buildQuery( - Arrays.asList( + List.of( named("avg(balance)", new AvgAggregator( - Arrays.asList(DSL.abs(ref("balance", INTEGER))), INTEGER))), - Arrays.asList(named("age", DSL.asin(ref("age", INTEGER)))))); + List.of(DSL.abs(ref("balance", INTEGER))), INTEGER))), + List.of(named("age", DSL.asin(ref("age", INTEGER)))))); } @Test @@ -341,10 +339,10 @@ void should_build_composite_aggregation_follow_with_order_by_position() { @Test void should_build_type_mapping_for_expression() { assertThat( - buildTypeMapping(Arrays.asList( + buildTypeMapping(List.of( named("avg(balance)", new AvgAggregator( - Arrays.asList(DSL.abs(ref("balance", INTEGER))), INTEGER))), - Arrays.asList(named("age", DSL.asin(ref("age", INTEGER))))), + List.of(DSL.abs(ref("balance", INTEGER))), INTEGER))), + List.of(named("age", DSL.asin(ref("age", INTEGER))))), containsInAnyOrder( map("avg(balance)", OpenSearchDataType.of(INTEGER)), map("age", OpenSearchDataType.of(DOUBLE)) @@ -362,9 +360,9 @@ void should_build_aggregation_without_bucket() { + " }%n" + "}"), buildQuery( - Arrays.asList( + List.of( named("avg(balance)", new AvgAggregator( - Arrays.asList(ref("balance", INTEGER)), INTEGER))), + List.of(ref("balance", INTEGER)), INTEGER))), Collections.emptyList())); } @@ -394,8 +392,8 @@ void should_build_filter_aggregation() { + " }%n" + "}"), buildQuery( - Arrays.asList(named("avg(age) filter(where age > 34)", - new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER) + List.of(named("avg(age) filter(where age > 34)", + new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER) .condition(DSL.greater(ref("age", INTEGER), literal(20))))), Collections.emptyList())); } @@ -443,18 +441,18 @@ void should_build_filter_aggregation_group_by() { + " }%n" + "}"), buildQuery( - Arrays.asList(named("avg(age) filter(where age > 34)", - new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER) + List.of(named("avg(age) filter(where age > 34)", + new AvgAggregator(List.of(ref("age", INTEGER)), INTEGER) .condition(DSL.greater(ref("age", INTEGER), literal(20))))), - Arrays.asList(named(ref("gender", OpenSearchDataType.of(STRING)))))); + List.of(named(ref("gender", OpenSearchDataType.of(STRING)))))); } @Test void should_build_type_mapping_without_bucket() { assertThat( - buildTypeMapping(Arrays.asList( + buildTypeMapping(List.of( named("avg(balance)", new AvgAggregator( - Arrays.asList(ref("balance", INTEGER)), INTEGER))), + List.of(ref("balance", INTEGER)), INTEGER))), Collections.emptyList()), containsInAnyOrder( map("avg(balance)", OpenSearchDataType.of(INTEGER)) @@ -490,9 +488,9 @@ void should_build_histogram() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("count(a)", new CountAggregator(Arrays.asList(ref("a", INTEGER)), INTEGER))), - Arrays.asList(named(span(ref("age", INTEGER), literal(10), ""))))); + List.of( + named("count(a)", new CountAggregator(List.of(ref("a", INTEGER)), INTEGER))), + List.of(named(span(ref("age", INTEGER), literal(10), ""))))); } @Test @@ -529,10 +527,10 @@ void should_build_histogram_two_metrics() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("count(a)", new CountAggregator(Arrays.asList(ref("a", INTEGER)), INTEGER)), - named("avg(b)", new AvgAggregator(Arrays.asList(ref("b", INTEGER)), INTEGER))), - Arrays.asList(named(span(ref("age", INTEGER), literal(10), ""))))); + List.of( + named("count(a)", new CountAggregator(List.of(ref("a", INTEGER)), INTEGER)), + named("avg(b)", new AvgAggregator(List.of(ref("b", INTEGER)), INTEGER))), + List.of(named(span(ref("age", INTEGER), literal(10), ""))))); } @Test @@ -564,9 +562,9 @@ void fixed_interval_time_span() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("count(a)", new CountAggregator(Arrays.asList(ref("a", INTEGER)), INTEGER))), - Arrays.asList(named(span(ref("timestamp", TIMESTAMP), literal(1), "h"))))); + List.of( + named("count(a)", new CountAggregator(List.of(ref("a", INTEGER)), INTEGER))), + List.of(named(span(ref("timestamp", TIMESTAMP), literal(1), "h"))))); } @Test @@ -598,9 +596,9 @@ void calendar_interval_time_span() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("count(a)", new CountAggregator(Arrays.asList(ref("a", INTEGER)), INTEGER))), - Arrays.asList(named(span(ref("date", DATE), literal(1), "w"))))); + List.of( + named("count(a)", new CountAggregator(List.of(ref("a", INTEGER)), INTEGER))), + List.of(named(span(ref("date", DATE), literal(1), "w"))))); } @Test @@ -632,17 +630,17 @@ void general_span() { + " }%n" + "}"), buildQuery( - Arrays.asList( - named("count(a)", new CountAggregator(Arrays.asList(ref("a", INTEGER)), INTEGER))), - Arrays.asList(named(span(ref("age", INTEGER), literal(1), ""))))); + List.of( + named("count(a)", new CountAggregator(List.of(ref("a", INTEGER)), INTEGER))), + List.of(named(span(ref("age", INTEGER), literal(1), ""))))); } @Test void invalid_unit() { assertThrows(IllegalStateException.class, () -> buildQuery( - Arrays.asList( - named("count(a)", new CountAggregator(Arrays.asList(ref("a", INTEGER)), INTEGER))), - Arrays.asList(named(span(ref("age", INTEGER), literal(1), "invalid_unit"))))); + List.of( + named("count(a)", new CountAggregator(List.of(ref("a", INTEGER)), INTEGER))), + List.of(named(span(ref("age", INTEGER), literal(1), "invalid_unit"))))); } @SneakyThrows diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/ExpressionAggregationScriptTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/ExpressionAggregationScriptTest.java index b98bc538ab..4675b6017d 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/ExpressionAggregationScriptTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/ExpressionAggregationScriptTest.java @@ -76,7 +76,7 @@ void can_execute_expression_with_integer_field_with_boolean_result() { @Test void can_execute_expression_with_text_keyword_field() { assertThat() - .docValues("name.keyword", "John") + .docValues("name.words", "John") .evaluate( DSL.equal(ref("name", OpenSearchTextType.of(Map.of("words", OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword)))), diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java index 521f93f2e7..4497d4aaee 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java @@ -9,17 +9,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; import static org.opensearch.core.xcontent.ToXContent.EMPTY_PARAMS; -import static org.opensearch.sql.data.type.ExprCoreType.DATE; -import static org.opensearch.sql.data.type.ExprCoreType.DATETIME; import static org.opensearch.sql.data.type.ExprCoreType.INTEGER; import static org.opensearch.sql.data.type.ExprCoreType.STRING; -import static org.opensearch.sql.data.type.ExprCoreType.TIME; -import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.opensearch.sql.expression.DSL.literal; import static org.opensearch.sql.expression.DSL.named; import static org.opensearch.sql.expression.DSL.ref; -import java.util.Arrays; import java.util.List; import java.util.Map; import lombok.SneakyThrows; @@ -31,7 +26,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opensearch.common.xcontent.XContentFactory; @@ -76,7 +70,7 @@ void should_build_bucket_with_field() { + " }\n" + "}", buildQuery( - Arrays.asList( + List.of( asc(named("age", ref("age", INTEGER)))))); } @@ -97,7 +91,7 @@ void should_build_bucket_with_literal() { + " }\n" + "}", buildQuery( - Arrays.asList( + List.of( asc(named(literal))))); } @@ -106,14 +100,14 @@ void should_build_bucket_with_keyword_field() { assertEquals( "{\n" + " \"terms\" : {\n" - + " \"field\" : \"name.keyword\",\n" + + " \"field\" : \"name.words\",\n" + " \"missing_bucket\" : true,\n" + " \"missing_order\" : \"first\",\n" + " \"order\" : \"asc\"\n" + " }\n" + "}", buildQuery( - Arrays.asList( + List.of( asc(named("name", ref("name", OpenSearchTextType.of(Map.of("words", OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword))))))))); } @@ -136,7 +130,7 @@ void should_build_bucket_with_parse_expression() { + " }\n" + "}", buildQuery( - Arrays.asList( + List.of( asc(named("name", parseExpression))))); } @@ -154,7 +148,7 @@ void terms_bucket_for_datetime_types_uses_long(ExprType dataType) { + " }\n" + "}", buildQuery( - Arrays.asList( + List.of( asc(named("date", ref("date", dataType)))))); } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/ExpressionFilterScriptTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/ExpressionFilterScriptTest.java index 61a3e9d35f..a1caf80e55 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/ExpressionFilterScriptTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/ExpressionFilterScriptTest.java @@ -92,7 +92,7 @@ void can_execute_expression_with_integer_field() { @Test void can_execute_expression_with_text_keyword_field() { assertThat() - .docValues("name.keyword", "John") + .docValues("name.words", "John") .filterBy( DSL.equal(ref("name", OpenSearchTextType.of(Map.of("words", OpenSearchDataType.of(OpenSearchDataType.MappingType.Keyword)))), diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilderTest.java index eb07076257..3a9d516060 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilderTest.java @@ -268,7 +268,7 @@ void should_use_keyword_for_multi_field_in_equality_expression() { assertJsonEquals( "{\n" + " \"term\" : {\n" - + " \"name.keyword\" : {\n" + + " \"name.words\" : {\n" + " \"value\" : \"John\",\n" + " \"boost\" : 1.0\n" + " }\n" @@ -286,7 +286,7 @@ void should_use_keyword_for_multi_field_in_like_expression() { assertJsonEquals( "{\n" + " \"wildcard\" : {\n" - + " \"name.keyword\" : {\n" + + " \"name.words\" : {\n" + " \"wildcard\" : \"John*\",\n" + " \"case_insensitive\" : true,\n" + " \"boost\" : 1.0\n" @@ -1681,10 +1681,12 @@ void cast_to_timestamp_in_filter() { + " }\n" + "}"; - assertJsonEquals(json, buildQuery(DSL.equal(ref("timestamp_value", TIMESTAMP), DSL - .castTimestamp(literal("2021-11-08 17:00:00"))))); - assertJsonEquals(json, buildQuery(DSL.equal(ref("timestamp_value", TIMESTAMP), DSL - .castTimestamp(literal(new ExprTimestampValue("2021-11-08 17:00:00")))))); + assertJsonEquals(json, buildQuery(DSL.equal( + ref("timestamp_value", OpenSearchDataType.of(TIMESTAMP)), + DSL.castTimestamp(literal("2021-11-08 17:00:00"))))); + assertJsonEquals(json, buildQuery(DSL.equal( + ref("timestamp_value", OpenSearchDataType.of(TIMESTAMP)), + DSL.castTimestamp(literal(new ExprTimestampValue("2021-11-08 17:00:00")))))); } @Test From 8b0671c1229e4aabad757aef7e17e6a4999b4a25 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 27 Jul 2023 17:44:23 -0700 Subject: [PATCH 11/12] Smart subfield selection. Signed-off-by: Yury-Fridlyand --- .../sql/opensearch/data/type/OpenSearchTextType.java | 9 +++++++-- .../sql/opensearch/data/type/OpenSearchTextTypeTest.java | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java index d50ff431c8..369bf4ca7a 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextType.java @@ -54,8 +54,13 @@ public String convertFieldForSearchQuery(String fieldName) { if (fields.size() == 0) { return fieldName; } - // Pick first field. What to do if there are multiple fields? + // Pick first string subfield (if present) otherwise pick first subfield. // Multi-field text support requested in https://github.com/opensearch-project/sql/issues/1887 - return String.format("%s.%s", fieldName, fields.keySet().toArray()[0]); + String subField = fields.entrySet().stream() + .filter(e -> e.getValue().getExprType().equals(STRING)) + .map(Map.Entry::getKey) + .findFirst() + .orElseGet(() -> fields.keySet().toArray(String[]::new)[0]); + return String.format("%s.%s", fieldName, subField); } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java index 9350a51be4..9ec2c8b7c8 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchTextTypeTest.java @@ -100,7 +100,7 @@ public void convertFieldForSearchQuery() { OpenSearchTextType.of(fieldsWithKeyword).convertFieldForSearchQuery("field")), () -> assertEquals("field.numbers", OpenSearchTextType.of(fieldsWithNumber).convertFieldForSearchQuery("field")), - () -> assertEquals("field.numbers", + () -> assertEquals("field.words", OpenSearchTextType.of(fieldsWithMixed).convertFieldForSearchQuery("field")) ); } From b04a92e1f1dfabeda0ab0c533a02edfcc66628a8 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 27 Jul 2023 20:39:44 -0700 Subject: [PATCH 12/12] Address PR feedback. Signed-off-by: Yury-Fridlyand --- .../opensearch/sql/data/type/ExprType.java | 11 +++++++++-- .../sql/data/type/ExprTypeTest.java | 16 ---------------- docs/dev/img/type-hierarchy-tree-final.png | Bin 30134 -> 32202 bytes docs/dev/query-type-conversion.md | 2 +- .../data/type/OpenSearchDataType.java | 9 ++++++++- .../data/type/OpenSearchDataTypeTest.java | 17 ++++++++++------- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java index c9310a75ac..33fe647a71 100644 --- a/core/src/main/java/org/opensearch/sql/data/type/ExprType.java +++ b/core/src/main/java/org/opensearch/sql/data/type/ExprType.java @@ -20,8 +20,8 @@ public interface ExprType { * Is compatible with other types. */ default boolean isCompatible(ExprType other) { - // Do double direction check with `equals`, because a derived class may override it - if (this.equals(other) || other.equals(this)) { + other = other.getExprType(); + if (getExprType().equals(other)) { return true; } else { if (other.equals(UNKNOWN)) { @@ -78,4 +78,11 @@ default String convertFieldForSearchQuery(String fieldName) { default Object convertValueForSearchQuery(ExprValue value) { return value.value(); } + + /** + * Get a simplified type {@link ExprCoreType} if possible. Used in {@link #isCompatible}. + */ + default ExprType getExprType() { + return this; + } } diff --git a/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java b/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java index ad3a111006..1035908a5f 100644 --- a/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java +++ b/core/src/test/java/org/opensearch/sql/data/type/ExprTypeTest.java @@ -50,22 +50,6 @@ public void isCompatible() { assertTrue(DATETIME.isCompatible(STRING)); } - @Test - public void isCompatibleTwoDirectionCheck() { - ExprType other = new ExprType() { - @Override - public String typeName() { - return null; - } - - @Override - public boolean equals(Object obj) { - return true; - } - }; - assertTrue(UNDEFINED.isCompatible(other)); - } - @Test public void isNotCompatible() { assertFalse(INTEGER.isCompatible(DOUBLE)); diff --git a/docs/dev/img/type-hierarchy-tree-final.png b/docs/dev/img/type-hierarchy-tree-final.png index 68309c5cf9c9453d1195baca9aea3a7aaae381f9..31793b557f66a8cf2f776eea3359f2b015a9c304 100644 GIT binary patch literal 32202 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 literal 30134 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

a})) zoKJy633~ROk_}ba4OVI9hllE$xC~?c`>Ivz{xQi0e=I4eGrFrn`Jw{lc}B6;J<@yg z!_JNM5#09X$}?_hV?@xBinh<#D~@()9T)7$T`En&FZsA1m0_dwk zvKVz)=D`TIE&7NJH?Wn*In`e}7$zg;q>8kexYo1BdJ1_uX`w4Kzh+e1`N{9H{BTVM z_vQHVh9;mG%y08WptFS3sM88_Ym()kSAQm-Macw@a=LkE9uE1u`v5TydKVlv5`2~% zB~TD&LX*TWCndx9o$@!$g7+sE_b3;wHt+ko^4e{PvESSTK%nASgO6IvP$wz1gq@)t z*R^Uohkd3x_)LaB%eFiU^z%*5bb<&N^ITnxSrf2jjI=cb-4!w0QSBclb*i!N?y(1dc?88 zTR)E?s1_7w_-EN9!fZRURdYOe;TO0apzUItV#S!u0$i(;_6gnT@NF5|{gEB4$;iZ6 zhpy0*Zvw)W2uZo^^;gkv>ZF+V9OMbVuv9qOa`OBzSGs-JPU9=Ln{x|yy5jQr`}f>G zV^o69Ct%vi^PdTSR-YBpL&JBU46HvX5SE+7#{0Hbwsu8m;B)v^Ov`%m$y@cr7p}tl zpYYzmU}-b6HRAsnCPHUhC^tX#sXwoqNr989Vh5c8RHI`M9Cj(gI_4JflH%O2H$59_ zc5?a3!=};AYzUi+DjmydDi8ApgX#~RKYld(?D}q&3uL z4{4BI<&f=I!01u+!`2IQ{Jsj_HiMTHCAfxwdukc%qa2SI+07Ok10NT7E$&lS_fXu` z16bdMD)PY^C&K;AlI)%jutgAFKC@{X7Ty_J=0V|=r_~(uT7*-!E}*w8!fEc`5mGh1 zk|qe!r4Fv33J2>+CC`#JaU2s7l(nCvp(EEYLMeBwo=Zw2XXEEL=cdw&mD1=xwSBNX zu}zUICHedAYE_i};}%bhjeM}|CwgINw#Jt8nU=lgljqqTt7j_iM+Mu*GPQYa=u%(o z!xDr;`LJg}U`$hC{CeGjllV`M4Rh)uo8sYy$)uH3c658q>Z(83rg`nTYqxu=M4K`K zsYMn0nci8L=e4+J=@ZtyFuFhGX2qq*c{q3N-R{TVYBuNY&wYFbm?hxd+K3>;yLjB~ zPEJyk6!lz>39qnoAoJuIzv>2k9AaF-^{ue0ahZ924aBh~uY$v9Lb9Ep`XwSHzz+K4isq>xDd+B{s9|DPJn zZ#A!QnrAb3E`i!NwgaLDkABh8@c;c;8QfsP_VAkH#*G&pfqsJ6@Wt-C%Rl&;&PJga z6lVfxFtp#|u%^g{Z)vJf?^l|QP~rY={zb1Q+xES0c?m($PfLfkW|uj7C>V1`6qAp7 z0-{k?=|U)GJ*cNZqKBK8E&4U(FKDZFL+U(0e|rK0^@-f@z-ZbJvw_W`xwYj?XupDI z1ZdLS`8mCDd*C}1eaY8c(3j|gJq+$B><$8_d{HTlA)nkfioU>JzjmVxJYU*0VT^*) zeRG^oC#LRzl~p?~f`-J|8Pt;gZ+h_m-f2XbxGzzJZ0p3z!Eb7IvNObh5$wo$pakgG zrv}&&!vNANXxj6eFMzs>vS0p_yLg{;JpLA{s6_UE6l4-1n}xPY2mJv&pRp;;VtZ3x zr~i-B|I5=Jcgg<^qwEL`*<^9$vG_yKcm#D|;f_7AvzZ5%3}{q{$MYLHdOPQtb<4)3 z*HQoE_w{YzL`UC}eYA8Le$V`;p-Td^s6)j~!@JF6h*=!xK!G0u-q@MFo3RYwLezKi%5IbiWe zHVSAk2RfOlz(y9KOKNlFWdhBoRX*(cJfJ;>OU>}3l+F>6$&Vl$&Ver5Q8-HcLNG2c ze9n)9YM~$YE<$_^gm~_GL|VL5Ri`?!{Gfbw%rz2WsaIipA|Xy)GjKg{2J>^moS(`!bshum|n{wY0j0O6ez3v9dU)4ec!Hof6Dbf zt@xe~cHog~BmbijtT=vn_WRw39f7yfu{DUJ4pI|RG z7X}3zmv3{qMOttp#!f>oJrp7L$m_1DR{@%+N%oqNpSkVrF@7}G(|0UsouKvZ9u1QR zAHt=)mQ2!5@z?zGFAH9gb3_qtp9EODZD13oH${i`e zqp|m%psI+N>Zg=a+Q0*gq@e`Ec%8@Jc0;Pl} zmd8~OSBH|hz!}#c4==4cjWW>_xw{naU%h;_nht=40;(WHfQ4nzJQNL@z3}Sga+*7! zG-SAJV(g4zIJFi1?VUweFL!DPi4`;|JV6ybK^-(s6ZQ(e4-DbwMRh2|RR8!<_5$!S zVFaC*sqk6L&@azR<{>)0=^dMstk1kU;j!_rlXKXWK`s>`Q6Z%QZE_f}@>gv^b_eru zclIMeZS8wob$1oo1XuL(VK|NuhzFkF?&=CK=Ow1Dc#-c_pJqpNAL?yr+a|9n{5Zgg z;C30)>l`eWH6Lu^Q~_})QjJvNz3aWXKro<|{Y-#`EU&Q4!DCsJE1#-Olt->M-2X_j zi16QFvkLz;)44CjhUcN7{50U8u`|6kIp@kw625UOu!>4E5TEP|%}0AX;acZQDpfFI z`epbe-F@h+>u&jMS1y;-7OWOmE!3?XcJt6N5W_~oDwK*JW}|_xaan23DLfXkp(Vd6 z%&j=@6o;L49d8!M=Z-{d6y?*W9B_H5-6#}isRrys;-ts)-0L8ZLxb4tTf+`qsjY$7 zr!0ZVL~L_MWwZPzaSUrP?Ignd^(r>APpHLukx?YP{z%(PTQD0yFLDv8T+Ut*r1FK9 z^t2p}dJU#^lxT)uxqS6|td|FZC8ge7YB%nA?6vJsxAKMjqF%v7$Q?O#Zp{M6#Dm?0 zd!Taw6xZw%A^?w+G2iu}LV!+seq@6HF*P(*%6!iCBFt*UP!ANRq z%EzS_2ec(6M#0Oi!P< z9-He}Etb;NXLxRmQl6I=bAVl&>gWbRl#MRz(hJw_e6P#!eaKlMS@z_^n?+8P;^Wyf zzI^bT9qrm}l;;J(@$o^yFakCG^F35beU@JJ0`Czf0$`-UOm8sky_XjdB~I%4Hg}AI z8eF2t#E%<1U((5(UE@3tce@sQ%wNT%bt4LY{O2d}I3FX?r-}fo4a*ihxh1=7JG;gY z+ns=z=_#U9{0~$4VUhiAIuL6~UD{f_Na7&Svlw5aS|k$F9)Kr{`{NvUs9~ zoMB>STtQAbCDHjskvNo#+XQtDnMB@4l}4Iz3NMLE_Gk?n zQBpzin~|chLxQ?|t^SQWTu=gS8aoOBX!_qKREM=W#TC&rC?ayORokintF-#3oiF>; z@e7c|XZQyoTJ9cKGJm~+6wc?HB_Ugv zFJ@V22cOx0%5@p7F5r}Ipo%nW3GpsXWdci=Rpj(Dn52zgg8xLCS2aU|kt+#dSLQ67 zrc2Z29hcv+{pk00$g1#2eN*jeZDK;1Iqo*3CXo!?ld)K1oFv0FL1OdNSwEtB2;mxA zv>#-ZkY?@8_ljl*&cYREP)T`SSfHZ-v|`(zbE|^J1w!nwt%ra3lnF3TQKR{_%>)eS zv){+8+EC|m=;ZB&f!*{U%x{eNO3JDAFRckR11bUHRZHPC-J3SIl812X-}Vn({XqKx zh%BP^emji2Rz&$jdk!maymXgoS$X7B=zA}cht`u_6$PN18|$UwxfhawSnc_WTVcao~ej zGGXXInEMNq%={nT*9q#5E?OBnTyl7@;>r=YH`Wtt23!v#&}^h-rf63}EQZ)zqbUZz2Xmbyd6Q*Z~{b!+i@Rvul4t-|{5ul80oK>E}=l@H%z8D$WLP3(dZ~ z)|=^!?~3SN)$3-i+meJ^a`0;zSyN)4lWs?U<(U`qiZYeKhaVCuZdo$Be6ctn`*d$5 zY(}oqbJvFI>eD*H#Y&oAW2}Z2Dg6e;0r^8S``nM--Pow8h#2=O;69@BA2jdFTp3^! zalB*Z{S;RPt6HnJA@_U!9Bix~@znmTPcCQUt*N#Gp-UC+OqZ~laps|;0xLf?4mBXZ z8J@?z1Wmn~$_vfX?)iT0#|v@<)<@hM83Fu{D(3H=P?>|BW$Keff;o2=VV~QJr#rKw zcaR!OSg`i%SQd>L%?gUr>-MvP&A6t1n&~e_Mh-h>KjlK^ZOAI^WwHC$dz~+F zGjV8wh1*GH45u~7=B#(a3G+*h1pV*zQ+o6}e#G&ZA7;i&D3{6A?~LiU$3wV|Bx5AF~`4C zoli75YZq4EzWzVBGLa-G8_7Rqx=ke3@ydCm2rE;CBZ&yhl2>{46jSk80#P@Pfvpzg zMLgd@*QHkEWu%p%Vq0@~yq73)okH*BBQWK?G0`=5OEqkM;$BHx4Y?C`*(_^!N7v=9 z&o9nDGj`s8bAOVp|H40)&I>sl{7jreuc8Q-vq?+L-+3g=o#atJ#e2g-jx6D*IEMF!t`Rk2u8uj zmnMe4JaliLUx>*0i+_(l#vz>J$JgIL06dr+kxfLQ&NJQ+c#Zd?$Ls%^@!KJaTNY;% zbI=_5zXmR^#xJkW13*0U^2M+6n8AY99^r5apB+U$qeHv0Q*Y3_vVA2u#%Hs&>$fa0 zq2Dtvi-I7V-r=(I?XYaxS)r<+TKK((+HWq*EhF3L(BU0{-QF06gcyU|Q0ZhwSh|m0 z)8|Bp6?mDKx4f)i_GP&Z5TosE%Uzz{=v?=61w`?_V{uT$fZI|lUX)g^pt>VPNFND2 zD~#>n?Rp=k>um7}Ep$hSph|!crm~ok4oUty09rHk0gf5r$hyw%5A%^qS^yAy5LBI# z*nDDsm`%&ZNU2f4(9;J*%onSBHnr^8CX0zv#t_H<-7g$Sg@|Wg$}J)R$tH=QN`!L= z9xSB7q1@@b=-bew!ZN#dNGTWsdVwOTkkNrW4wCE8mTU6RQI!Ff^62A9k5rJJ7r$i_ zK(anEzZFJjk?};B<9G6+nJ3N89#~857vp}#}hZxipYYL%o2?-^4r(YmOvF5OibakM0fg$ zmF57{egbRHeTvUXD#2Yhdo>xUHOZRdRA3vm8$sS}+dB?$tWR(bcjljmR9;s{`aV9l zC!ZBi&|}gTicwVL&PG{t@!{W^o`ruS7W*ZCx)!cqp)k)r=X}>wH%H8bS0)lnA8dCr z0x~A5I6K3TM*J01x&k{6j>KUmT189%zK!RLmycpp+e9_sWwkMY?nzpnhDSvec^Jz{ z-MfJjC6TD1IWSY$GcBf6zaw}uc}skTKY$q{C(dT_=e4#|EX zm>HTwL}+ODlizRXHGW3q z?_AF015mhme6S?}zz&fAmyiqs-yI3xU9(n&qg3_5WsyV`q7_ax$i0qpka2om9zJF)VAMBuYke(1(c3%jI zLB2i0`Vcutl1?zc zimsA7qiw2rul`*~p-p#3_%}#^3qm;UBr21ZlD`f7*K-tOeRIvW{TQ0h#enslz}V-3 zK#ZQ#YfB46eFP9=pI7FF{?yn~JhSJdL7*?2=!T&YPECyGK0G>_`!5twZTskSK9P0! z1+6`Aj{r>!uFT7aqDSL$gpfLzfus&BDWi*!${m*^MDCZU64V_En@C@pf4)WHp5=1* z(d|Qz7}QP)6D9kH0?V{>KKFr5RI_Pez;A{TN%x8}?OP$*wyAgB=byF>FYF-Fx!KX| zKm38uH?RpxN|4M)$NW&)jyK(2FQGh%rKA_v_!UaZfG7oa6D3$0fZb%D(C~OV&t&e# zfVN!*A56R(fp7+wAWw1%TFWYy&<%UenC~Y1^w=9a8+sVY1AlML_;n~Df5;7h0)z+S zZh+J+LQ40ul7BYJHtcyKw?NNlyZXQZw?pOn~=i#?}@M_U+9v7UR z&JlG7SN>lI4%1|H9 zNtIFQS2_4bpk6!9Kmfs;d6Nv zyy>f5j9jF^aV3ct{KsV-MJS))WQYm~<@TMQ4Ye1*jz`J~#I}D2p}nfnzwlV)Cc$V@ zfB6#pL&pc4>bW6r4RU(BtLMa(n|D#$l1yeIM13Ut@@vx?bRf+yvdCpovlzKL(wy|7 zhf^d~y6pF8cL{v3=RE$!1wk&=w#k4(?fiokS442wlj}v^PdSk)eXi<^FT8>#4bCtJ zY)~OKM859rZ*U$^ubshLPpBy^0D4BEcB{(B!2oQ`*^|uFHikiSwKS|0aS38mCySW$ z;1?q2zmt*s<6YFrjUzHpfb!~5pw&`lWZ)uP>-sm(qGo=n-Zu11f8G)3?olcao-=5_ zRZy$51nP$2#w=Tif4=sSUB;(Z^wM*$a=s5j#3pe9^dG*P@z~*{rKB-0+(^P-++r{T2Xg_tk zQGrZx;kcpBfJpnIbsts`u!ua?1kQVp=(=|8J+BranaX0PSq0^z^pU?`u78^N4)h4b zr6i@%*>ts$t7tkmKLw5Eoxa*a@rWtN#+Vk1B&i-s=T?BiYU>zr4EOm7SPOLBpb5~v zLzSwK#j7KW#Ur;?4#j5(Omc)z{S-dNo7grBUsLFztCN=fQ$L+5Ux{yVdF!I|HY(4{ zB`6z5*h`9}|3zFF;e3LyZ_UV#&ib<<>RJtFgItg5Q`3_Ztk+Yax82~k3nEPt%`L4G zz7zIenR!Y%%H_lt%AnM&eIzxH1X8HRWVNOySTg-*Xv<90euNeuH_} zQMt-br1TPm4OT;E$}*Ud`kmha9@W>)bTjff)g{z+RtWyn*U<;7o8YllOg$Pk43Q*% z7=DG)`YV)kYep4xiqd-g=g(jTQ>a+U!giZuuBT(bcp}BL`1d{eerUBN1H3LSZ)0cD0eKN zUgW4kJ}&vt;h~C0!Y-Zz-6Bo9)h=dlQ{z{wd3{|v(yaUlPRZf6id&d^9bSw&mQGm<_TUy0^ktP)!F@^)WR`s1oYC`;k`>(9bwUTn_bA}AC`!Y=ky zeeeo{B4*t{s3z@W_eSw?iaslHo}>Eazg8Spz3K(pfJsx@6O)9S`P>d5EbTxCE~#3t zOZck08n>g0_PXS-P@zN#-~F0%HA2?9mf3gHtci6Cr*+*xwDfTrzce$6ffV>|ypG)x zzFpWo3T9O(+^^)my46c{-(%1uEJ(WIi*1h5DrXIhI*VGK5JQc& z7u%4CR70nbw2_F-picyP$*jV!bW8mHu>`jnZ*??#IQ@?lGhwjfyY=!DIRaw%lC1DwBH1X+9JacE29DW}5vMPy4r()k9jCV4@y- zW(8gi@(lspiWNb)m{<45cwW$LFLfKT1XP_-pSa^Ufp7pu0f zG8Ms1>e8-Di7ZKd6;$*gjw#TNED$^4y1F+oK-pzZDWs_fTA8QM+nk&)M?lTJVI7@R zUzJP`HP8JCR96wgk+|)b6?y;YcEdWk2G{ecLE7OSNDEF;N6qe8$Z6*W=mJq#-Tl}& z9_y9AJ+TdlcbJdRJ!Kk1$>NcrGxZu^9YEFG=s422$n7ZZg2_bt4kR|`NiGdSJ8-^P zjfMo>u)!ap7s`Kk7%1Qp>jM0ZUN-{m^4=c))k4)LTE+S2e{N+HAl3%rQ5YS_B}a26 zM{XRy%JwP*C`B1!?5rsV?q040#!nbDo*a}T;eZkaO({JOqoE1tf(;8n7a)0AR$e!- zf(|(MXC1WPWB0lbLAb+dGm_hpV2)Q83c(!aPY|<%_ffp`%t7?nm#01&?G)q?y793_W(}bYd{y zX71tc3rZ%lMKPA=XN0MamTN%=U1(iy>koDat+$qJC%Nk3AsxT?o5ZvTiqyNYnxZR5 z0NE3_(WC_}x&*!aa-m593YDHYI8{(2+l_%6vLirW?Z_pI9;B75QDa5ZMy#ARl!66H z>KYP!en1m;+FgR6iq^!>w!=5@jY(~H*Yq}Ka}S9KLY@srzRdF~c(kNaYjCYce@)nL zl#V?ld=pJMFhc9&g34wh3F&MEEJKWww+Qusu8$reIWJvq&G@7*cbed`M)gb7Y+OHa znW=t16tkK`C2OymwSR7N>Foj19Nf~oqjA~TtU%30NjX4&#w6-)KXB})b~0369Y;M~ zh<(PU@);MtjSf1C>3JLV=b-2pQzH+htxa3{I)V#!Q>SDY15gC99ZCg)@DXF_{N$7= z=@2~w0U|9nLPJ3y{QZguG0;+}6g0xYXY27m(8NaRDvURW7^EK)5+}@fpJow)RPoT- zL|kUnofvx?mQd10`FUUrG=Z*O7UKFk%$r5At%vNQ&vc0qM^VQk5&Ijh?CGekMG$oi z;W9!qxZb}CZR0EMK#1|w5OqWp!3&r-At;6sREuC3P$VqE7ci;TB5)oQs8aVmlzuo0 zV^E|-I0vRC8co*bIw*}nk9L=Ota1Xm0T>Z0X^*j@o7(!p>sS%PaQD*9 zU2VcCR~F&El|k5>co_Mu*V(62T)-C=*}uoelD^nbx7OTeMYb#O>h96d#wBnQ0fYEC zun31Pgk4uV;M8rCQ|jW=gVTB;Ukw@Akbbw8}@f^WSbT zycxa4#eKwakwMS%D496+vPlmgrfnjeYmu-mgoe*-%0WVQh`9tNFfzm*2nsJy9x&1g z0_gy@1WJ9R2h4UpTBi6sW5+J)3vc0pw;hWn-~KUFbQ1q26)DQT>?=oXz0DeK^Eg)-?lIy`KqV`f%qtE3&&-t-Jia6C8_*6oV&zir zfR-Lq%Sw3~kM%&sq!va@nwYwYAnK)z!PtU(1BPr&t-va3=8fu;nG2||t3)Y6^!9fH z^$$&}Zj@6VduC6)*SoC4+MnVva!bKxDI6;Nr3A)dm*@bL1vQgLmD703u&1e}1aJb~ zf5!<%@5ybivNDr$E$E7JsdgL;7k41L68z7u5# zez!Qk?(-;uj_Ll_Ycp=3MH{_r7@SJ9_-Z*YkAJs@h~o}+t6}S$eoJ*dQ8MipK96|= z5YH-T8w3s!`%Cvl{SXg5px2El%h9*%6cpU`3%tafP1u6VNJ3mxH825#H`Hi@LgZ1O z4sA3qI$}`T0J&|Z7vDp)_P3d1mETCeK(Q6UJRjcy7lbmI814}h@<|{HWbQ$e5ySzD z5H|Jl%Bk>|VereyBbjMUkWanIL>ivg+(W({_a2aJzr=&T`*VgXtr`0|hnwh516p6~ znI(sGIs9lA^ak%8PI?Ak4-9--$sqfWiQjfYX7QH5f#kY6nrIaScl75Zp05FzGo-&h z8Y5vqkdaCRBp5>M9NKvQS~IRX11<4J1#*0XYq6-G{=Z5r+FBnmXa%(Vj?YjP{(C3T7x3tIRpOMl+1sTTZ|{b!~d$mCjf(1Voc>CVdUqWHk68r0c!7< z7^e{RIc@=1!2K|@yMN`8z>zmaYQ^fS#61d7HwmL=HyCWPsr(&Of`Ur^Chly677BxM zoECs|wS@oi%&kR`T+#_Dg5*MD8MK{0(4TenpG?PC>LRHaBARG^dv=So57ORzxu^o> zOH4_0+fj~sUDgy>0@ANlzh0~%k-usIzEtEuAx(ZU80f-@Ifyp9L4JeeS4DrB!#k$; z=?2$jh}xfa=O6Aqa?WElv*-g}xP;>>;>fn6`@r`3M4m|I-ERWK+!w=ZFKE zReV0rAw3$3YPY7l`#=-}g^^>Dj8YKoxUgcv*y3lRywsd~wSAk9D z1uojAmzR;n?ovrR4_wEQqubCh+J)fW+oIDp7kcKMk&aTS!J!IL#eh7-fY3TBY6%nx zLXeP9TNDK%kAR_Km|CS4p`}qV&=5O85J5tvkVyn$L2M}qtpOuO9!emR2pJNCkdUNj zNzhtn+RpT2yPvXW@7=vObIJTP2*8R zNzW~uCKz~K6jWUJ0`C4}D!j4`L8|S`ZEF#5orHN((*DxZv?lfrQ~|L}b=7xwqw&qt zn(fHJF#H)&(O{7GTb20-FI)ScYLN7DgZ%A6m{F8~v3q6QGsC&wkjk5x3=&1YI(nxE z^!njs1O}uHK{;`MAx#`*KB|i`WlD5=^U^rA`H|NlzcPW&9Y_$FNW5+tMIl5GH#|-fQ1%tR_Vo(h4B(P!c1vKdRaT6CI>|u5v zeR}*g=4SB@iP;H+P|({Dg7|@y!FWTl1%v4WFmz9YNEyqzr1#}ukPyOlt=!nKT z7#;;2@sz8Ei_seCy<)9KZIZbx{Wha?&er$iL48u#s!05mc10yh z=UL;UM!)64uWXyEkPZgRAb?W~vaY!x$l80e%5z{ueo40?1rWp%odLF0q;zYYY+K7} z1sJLQkF4iJuqi2X$DMRmBlR$ZJ!Dj@pxIydFzKc0nalBw?vImQKx`jDL>XwhRAqtR zCw4o2F7V@0uE7cWfJy{`69kRplAz42FHxu)AEFxm%TTv}1i-7g|6+b?)qg)*rd83~ zs|8AYZlRcIY+imxk7k2-xYAN&b+eORH2~rI^!j*W``pBLW3AG=f0ma)KTdp}z>($H zH!v+bcGl4Djy^U)`mg3bi6tx3Dx5nAtS8}8Hbv?gIv}rSWg3krO|CLd>EkXo$yFPo zy*CRvryje_ihb#62}34LHtONP^NSp-YD<(%r`kTDd{C|nomUqwt&sKvE+h_FG(T={ zAoj}ZBU-ad7r&;Xk2v0_;B$zo7(yX=W<^_DR*3;${D1;VVlbZjPcNwCcnLv)CFU6# zhMidSIbReUwP_QBqF*;(AslO^tcmRQ9Hogm9~kwxWjJn?p2<;|Qb5Y~d0WP=EKH4h zM%By=I*%V9ADGyg%}_Lsm7z7$O5c|xN(0?GPG*M**e6=<_|$Y7hauo@7BEPyt923( z6kYs%UhXteFkN`7#c&v{%3D-KEM(A4>F14$w$>%3dp18RSlI5^%24AlFSnbSOm2)0 zxfww&$EuDu)+QOyX*1tNPItC7Hu&Yv!{l%E$$wiXC7r=48pVD=C`=0&hgIo!<@KX8 zt@PA@WGN?Q)+4z}E+aMcJ*8I;=ZtG#y|g&KZ-xSE4|wN7 zvNpuNS~MnX)8n~}Bg&21z~$ZM60@jBv+KwObB6z?aV+(G2!fs^EuQEZasHLj<3eDb zqxM1+d5r}px4uMtl#?kmwH;4n$vPEVJiq6=`r>2#-)%4qzm6l8=b5`$yfw?Jxd|!E z&2w%I{RkP?uAb^8lp%SpiL=2B|x zmMB%8eKC8Ojh^RMvfX@XZ3S->_fsG-#xjsp70w#n$D6DGI@^(H89M-|by)bWICVG? zT&I;gSsv<&Gty7Vt7KKYZCPfG()KIp%^a=pe=6m_z;34aO&OI>wWnfl+E43T&OpenSearchDataTypeTest::traverseAndFlatten() test for example. * @param tree A list of `OpenSearchDataType`s - map between field name and its type. * @return A list of all `OpenSearchDataType`s from given map on the same nesting level (1). * Nested object names are prefixed by names of their host. diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java index 3fcccaa832..2756f604a4 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import org.opensearch.sql.data.model.ExprDoubleValue; import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.data.type.ExprType; @@ -450,12 +451,14 @@ private Map getSampleMapping() { @Test public void getExprType() { - assertEquals(OpenSearchTextType.of(), - OpenSearchDataType.of(MappingType.Text).getExprType()); - assertEquals(FLOAT, OpenSearchDataType.of(MappingType.Float).getExprType()); - assertEquals(FLOAT, OpenSearchDataType.of(MappingType.HalfFloat).getExprType()); - assertEquals(DOUBLE, OpenSearchDataType.of(MappingType.Double).getExprType()); - assertEquals(DOUBLE, OpenSearchDataType.of(MappingType.ScaledFloat).getExprType()); - assertEquals(TIMESTAMP, OpenSearchDataType.of(MappingType.Date).getExprType()); + assertAll( + () -> assertEquals(OpenSearchTextType.of(), + OpenSearchDataType.of(MappingType.Text).getExprType()), + () -> assertEquals(FLOAT, OpenSearchDataType.of(MappingType.Float).getExprType()), + () -> assertEquals(FLOAT, OpenSearchDataType.of(MappingType.HalfFloat).getExprType()), + () -> assertEquals(DOUBLE, OpenSearchDataType.of(MappingType.Double).getExprType()), + () -> assertEquals(DOUBLE, OpenSearchDataType.of(MappingType.ScaledFloat).getExprType()), + () -> assertEquals(TIMESTAMP, OpenSearchDataType.of(MappingType.Date).getExprType()) + ); } }