diff --git a/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java b/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java index c7303d9adea..49cb8ac1eec 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java +++ b/core/src/main/java/org/opensearch/sql/calcite/ExtendedRexBuilder.java @@ -5,14 +5,10 @@ package org.opensearch.sql.calcite; -import static org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.ExprUDT.EXPR_DATE; -import static org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.ExprUDT.EXPR_TIME; -import static org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.ExprUDT.EXPR_TIMESTAMP; - import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.List; -import java.util.Set; +import java.util.Locale; import org.apache.calcite.avatica.util.TimeUnit; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rex.RexBuilder; @@ -24,7 +20,11 @@ import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeUtil; import org.opensearch.sql.ast.expression.SpanUnit; -import org.opensearch.sql.calcite.type.ExprSqlType; +import org.opensearch.sql.calcite.type.AbstractExprRelDataType; +import org.opensearch.sql.calcite.utils.OpenSearchTypeFactory; +import org.opensearch.sql.data.type.ExprCoreType; +import org.opensearch.sql.exception.ExpressionEvaluationException; +import org.opensearch.sql.exception.SemanticCheckException; import org.opensearch.sql.expression.function.PPLBuiltinOperators; public class ExtendedRexBuilder extends RexBuilder { @@ -124,16 +124,31 @@ public RexNode makeCast( // SqlStdOperatorTable.NOT_EQUALS, // ImmutableList.of(exp, makeZeroLiteral(exp.getType()))); } - } else if (type instanceof ExprSqlType exprSqlType - && Set.of(EXPR_DATE, EXPR_TIME, EXPR_TIMESTAMP).contains(exprSqlType.getUdt())) { - switch (exprSqlType.getUdt()) { - case EXPR_DATE: - return makeCall(type, PPLBuiltinOperators.DATE, List.of(exp)); - case EXPR_TIME: - return makeCall(type, PPLBuiltinOperators.TIME, List.of(exp)); - case EXPR_TIMESTAMP: - return makeCall(type, PPLBuiltinOperators.TIMESTAMP, List.of(exp)); - } + } else if (OpenSearchTypeFactory.isUserDefinedType(type)) { + var udt = ((AbstractExprRelDataType) type).getUdt(); + var argExprType = OpenSearchTypeFactory.convertRelDataTypeToExprType(exp.getType()); + return switch (udt) { + case EXPR_DATE -> makeCall(type, PPLBuiltinOperators.DATE, List.of(exp)); + case EXPR_TIME -> makeCall(type, PPLBuiltinOperators.TIME, List.of(exp)); + case EXPR_TIMESTAMP -> makeCall(type, PPLBuiltinOperators.TIMESTAMP, List.of(exp)); + case EXPR_IP -> { + if (argExprType == ExprCoreType.IP) { + yield exp; + } else if (argExprType == ExprCoreType.STRING) { + yield makeCall(type, PPLBuiltinOperators.IP, List.of(exp)); + } + // Throwing error inside implementation will be suppressed by Calcite, thus + // throwing 500 error. Therefore, we throw error here to ensure the error + // information is displayed properly. + throw new ExpressionEvaluationException( + String.format( + Locale.ROOT, + "Cannot convert %s to IP, only STRING and IP types are supported", + argExprType)); + } + default -> throw new SemanticCheckException( + String.format(Locale.ROOT, "Cannot cast from %s to %s", argExprType, udt.name())); + }; } return super.makeCast(pos, type, exp, matchNullability, safe, format); } diff --git a/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java b/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java index 57e43d93897..cf231401fd1 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java +++ b/core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java @@ -259,7 +259,8 @@ public static String getLegacyTypeName(RelDataType relDataType, QueryType queryT /** Converts a Calcite data type to OpenSearch ExprCoreType. */ public static ExprType convertRelDataTypeToExprType(RelDataType type) { - if (type instanceof AbstractExprRelDataType udt) { + if (isUserDefinedType(type)) { + AbstractExprRelDataType udt = (AbstractExprRelDataType) type; return udt.getExprType(); } ExprType exprType = convertSqlTypeNameToExprType(type.getSqlTypeName()); @@ -326,4 +327,14 @@ public Type getJavaClass(RelDataType type) { } return super.getJavaClass(type); } + + /** + * Whether a given RelDataType is a user-defined type (UDT) + * + * @param type the RelDataType to check + * @return true if the type is a user-defined type, false otherwise + */ + public static boolean isUserDefinedType(RelDataType type) { + return type instanceof AbstractExprRelDataType; + } } diff --git a/core/src/main/java/org/opensearch/sql/data/model/ExprIpValue.java b/core/src/main/java/org/opensearch/sql/data/model/ExprIpValue.java index f5391d864c0..7723ee8c689 100644 --- a/core/src/main/java/org/opensearch/sql/data/model/ExprIpValue.java +++ b/core/src/main/java/org/opensearch/sql/data/model/ExprIpValue.java @@ -45,7 +45,8 @@ public boolean equal(ExprValue other) { @Override public String toString() { - return String.format("IP %s", value()); + // used for casting to string + return value(); } @Override diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java index 47b4f44f32e..02ddfd78dee 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java @@ -75,6 +75,7 @@ import org.opensearch.sql.expression.function.udf.datetime.YearweekFunction; import org.opensearch.sql.expression.function.udf.ip.CidrMatchFunction; import org.opensearch.sql.expression.function.udf.ip.CompareIpFunction; +import org.opensearch.sql.expression.function.udf.ip.IPFunction; import org.opensearch.sql.expression.function.udf.math.CRC32Function; import org.opensearch.sql.expression.function.udf.math.ConvFunction; import org.opensearch.sql.expression.function.udf.math.DivideFunction; @@ -281,6 +282,9 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable { NullPolicy.ARG0, PPLOperandTypes.DATETIME_OR_STRING) .toUDF("TIME"); + + // IP cast function + public static final SqlOperator IP = new IPFunction().toUDF("IP"); public static final SqlOperator TIME_TO_SEC = adaptExprMethodToUDF( DateTimeFunctions.class, diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java index dc4f195d6be..f9a713fc49a 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java @@ -528,18 +528,9 @@ public void registerOperator(BuiltinFunctionName functionName, SqlOperator... op // Comparison operators like EQUAL, GREATER_THAN, LESS_THAN, etc. // SameOperandTypeCheckers like COALESCE, IFNULL, etc. register(functionName, wrapWithComparableTypeChecker(operator, comparableTypeChecker)); - } else if (typeChecker instanceof UDFOperandMetadata.IPOperandMetadata) { - register( - functionName, - createFunctionImpWithTypeChecker( - (builder, arg1, arg2) -> builder.makeCall(operator, arg1, arg2), - new PPLTypeChecker.PPLIPCompareTypeChecker())); - } else if (typeChecker instanceof UDFOperandMetadata.CidrOperandMetadata) { - register( - functionName, - createFunctionImpWithTypeChecker( - (builder, arg1, arg2) -> builder.makeCall(operator, arg1, arg2), - new PPLTypeChecker.PPLCidrTypeChecker())); + } else if (typeChecker + instanceof UDFOperandMetadata.UDTOperandMetadata udtOperandMetadata) { + register(functionName, wrapWithUdtTypeChecker(operator, udtOperandMetadata)); } else { logger.info( "Cannot create type checker for function: {}. Will skip its type checking", @@ -558,6 +549,13 @@ private static SqlOperandTypeChecker extractTypeCheckerFromUDF( return (udfOperandMetadata == null) ? null : udfOperandMetadata.getInnerTypeChecker(); } + // Such wrapWith*TypeChecker methods are useful in that we don't have to create explicit + // overrides of resolve function for different number of operands. + // I.e. we don't have to explicitly call + // (FuncImp1) (builder, arg1) -> builder.makeCall(operator, arg1); + // (FuncImp2) (builder, arg1, arg2) -> builder.makeCall(operator, arg1, arg2); + // etc. + /** * Wrap a SqlOperator into a FunctionImp with a composite type checker. * @@ -624,6 +622,21 @@ public PPLTypeChecker getTypeChecker() { }; } + private static FunctionImp wrapWithUdtTypeChecker( + SqlOperator operator, UDFOperandMetadata.UDTOperandMetadata udtOperandMetadata) { + return new FunctionImp() { + @Override + public RexNode resolve(RexBuilder builder, RexNode... args) { + return builder.makeCall(operator, args); + } + + @Override + public PPLTypeChecker getTypeChecker() { + return PPLTypeChecker.wrapUDT(udtOperandMetadata.allowedParamTypes()); + } + }; + } + private static FunctionImp createFunctionImpWithTypeChecker( BiFunction resolver, PPLTypeChecker typeChecker) { return new FunctionImp1() { diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java index b3de4de4f51..df2225ab9c7 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLTypeChecker.java @@ -23,7 +23,6 @@ import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeUtil; -import org.opensearch.sql.calcite.type.ExprIPType; import org.opensearch.sql.calcite.utils.OpenSearchTypeFactory; import org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils; import org.opensearch.sql.data.type.ExprCoreType; @@ -257,53 +256,6 @@ public String getAllowedSignatures() { } } - class PPLIPCompareTypeChecker implements PPLTypeChecker { - @Override - public boolean checkOperandTypes(List types) { - if (types.size() != 2) { - return false; - } - RelDataType type1 = types.get(0); - RelDataType type2 = types.get(1); - return areIpAndStringTypes(type1, type2) - || areIpAndStringTypes(type2, type1) - || (type1 instanceof ExprIPType && type2 instanceof ExprIPType); - } - - @Override - public String getAllowedSignatures() { - // Will be merged with the allowed signatures of comparable type checker, - // shown as [COMPARABLE_TYPE,COMPARABLE_TYPE] - return ""; - } - - private static boolean areIpAndStringTypes(RelDataType typeIp, RelDataType typeString) { - return typeIp instanceof ExprIPType && typeString.getFamily() == SqlTypeFamily.CHARACTER; - } - } - - class PPLCidrTypeChecker implements PPLTypeChecker { - @Override - public boolean checkOperandTypes(List types) { - if (types.size() != 2) { - return false; - } - RelDataType type1 = types.get(0); - RelDataType type2 = types.get(1); - - // accept (STRING, STRING) or (IP, STRING) - if (type2.getFamily() != SqlTypeFamily.CHARACTER) { - return false; - } - return type1 instanceof ExprIPType || type1.getFamily() == SqlTypeFamily.CHARACTER; - } - - @Override - public String getAllowedSignatures() { - return "[STRING,STRING],[IP,STRING]"; - } - } - /** * Creates a {@link PPLFamilyTypeChecker} with a fixed operand count, validating that each operand * belongs to its corresponding {@link SqlTypeFamily}. @@ -379,6 +331,42 @@ static PPLComparableTypeChecker wrapComparable(SameOperandTypeChecker typeChecke return new PPLComparableTypeChecker(typeChecker); } + /** + * Create a {@link PPLTypeChecker} from a list of allowed signatures consisted of {@link + * ExprType}. This is useful to validate arguments against user-defined types (UDT) that does not + * match any Calcite {@link SqlTypeFamily}. + * + * @param allowedSignatures a list of allowed signatures, where each signature is a list of {@link + * ExprType} representing the expected types of the function arguments. + * @return a {@link PPLTypeChecker} that checks if the operand types match any of the allowed + * signatures + */ + static PPLTypeChecker wrapUDT(List> allowedSignatures) { + return new PPLTypeChecker() { + @Override + public boolean checkOperandTypes(List types) { + List argExprTypes = + types.stream().map(OpenSearchTypeFactory::convertRelDataTypeToExprType).toList(); + for (var allowedSignature : allowedSignatures) { + if (allowedSignature.size() != types.size()) { + continue; // Skip signatures that do not match the operand count + } + // Check if the argument types match the allowed signature + if (IntStream.range(0, allowedSignature.size()) + .allMatch(i -> allowedSignature.get(i).equals(argExprTypes.get(i)))) { + return true; + } + } + return false; + } + + @Override + public String getAllowedSignatures() { + return PPLTypeChecker.getExprFamilySignature(allowedSignatures); + } + }; + } + // Util Functions /** * Generates a list of allowed function signatures based on the provided {@link @@ -464,6 +452,10 @@ private static String getFamilySignature(List families) { List> signatures = Lists.cartesianProduct(exprTypes); // Convert each signature to a string representation and then concatenate them + return getExprFamilySignature(signatures); + } + + private static String getExprFamilySignature(List> signatures) { return signatures.stream() .map( types -> diff --git a/core/src/main/java/org/opensearch/sql/expression/function/UDFOperandMetadata.java b/core/src/main/java/org/opensearch/sql/expression/function/UDFOperandMetadata.java index a7d12cbbbaf..dc4761b26e7 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/UDFOperandMetadata.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/UDFOperandMetadata.java @@ -17,6 +17,7 @@ import org.apache.calcite.sql.type.SqlOperandMetadata; import org.apache.calcite.sql.type.SqlOperandTypeChecker; import org.apache.calcite.sql.validate.SqlUserDefinedFunction; +import org.opensearch.sql.data.type.ExprType; /** * This class is created for the compatibility with {@link SqlUserDefinedFunction} constructors when @@ -105,47 +106,11 @@ public String getAllowedSignatures(SqlOperator op, String opName) { }; } - /** - * A named class that serves as an identifier for IP comparator's operand metadata. It does not - * implement any actual type checking logic. - */ - class IPOperandMetadata implements UDFOperandMetadata { - @Override - public SqlOperandTypeChecker getInnerTypeChecker() { - return this; - } - - @Override - public List paramTypes(RelDataTypeFactory typeFactory) { - return List.of(); - } - - @Override - public List paramNames() { - return List.of(); - } - - @Override - public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure) { - return false; - } - - @Override - public SqlOperandCountRange getOperandCountRange() { - return null; - } - - @Override - public String getAllowedSignatures(SqlOperator op, String opName) { - return ""; - } + static UDFOperandMetadata wrapUDT(List> allowSignatures) { + return new UDTOperandMetadata(allowSignatures); } - /** - * A named class that serves as an identifier for cidr's operand metadata. It does not implement - * any actual type checking logic. - */ - class CidrOperandMetadata implements UDFOperandMetadata { + record UDTOperandMetadata(List> allowedParamTypes) implements UDFOperandMetadata { @Override public SqlOperandTypeChecker getInnerTypeChecker() { return this; diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CidrMatchFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CidrMatchFunction.java index 1e326ccb7a9..c3a4fe4efe6 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CidrMatchFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CidrMatchFunction.java @@ -17,6 +17,7 @@ import org.opensearch.sql.data.model.ExprIpValue; import org.opensearch.sql.data.model.ExprValue; import org.opensearch.sql.data.model.ExprValueUtils; +import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.expression.function.ImplementorUDF; import org.opensearch.sql.expression.function.UDFOperandMetadata; import org.opensearch.sql.expression.ip.IPFunctions; @@ -46,7 +47,10 @@ public UDFOperandMetadata getOperandMetadata() { // EXPR_IP is mapped to SqlTypeFamily.OTHER in // UserDefinedFunctionUtils.convertRelDataTypeToSqlTypeName // We use a specific type checker to serve - return new UDFOperandMetadata.CidrOperandMetadata(); + return UDFOperandMetadata.wrapUDT( + List.of( + List.of(ExprCoreType.IP, ExprCoreType.STRING), + List.of(ExprCoreType.STRING, ExprCoreType.STRING))); } public static class CidrMatchImplementor implements NotNullImplementor { diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CompareIpFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CompareIpFunction.java index 3821ec1695a..edb898f3e0f 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CompareIpFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/CompareIpFunction.java @@ -15,6 +15,7 @@ import org.apache.calcite.sql.type.ReturnTypes; import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.opensearch.sql.data.model.ExprIpValue; +import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.expression.function.ImplementorUDF; import org.opensearch.sql.expression.function.UDFOperandMetadata; @@ -66,7 +67,11 @@ public SqlReturnTypeInference getReturnTypeInference() { @Override public UDFOperandMetadata getOperandMetadata() { - return new UDFOperandMetadata.IPOperandMetadata(); + return UDFOperandMetadata.wrapUDT( + List.of( + List.of(ExprCoreType.IP, ExprCoreType.IP), + List.of(ExprCoreType.IP, ExprCoreType.STRING), + List.of(ExprCoreType.STRING, ExprCoreType.IP))); } public static class CompareImplementor implements NotNullImplementor { diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/IPFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/IPFunction.java new file mode 100644 index 00000000000..e8143b9b384 --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/ip/IPFunction.java @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.udf.ip; + +import java.util.List; +import java.util.Locale; +import org.apache.calcite.adapter.enumerable.NullPolicy; +import org.apache.calcite.adapter.enumerable.RexToLixTranslator; +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.sql.type.ReturnTypes; +import org.apache.calcite.sql.type.SqlReturnTypeInference; +import org.opensearch.sql.calcite.utils.OpenSearchTypeFactory; +import org.opensearch.sql.data.model.ExprIpValue; +import org.opensearch.sql.data.type.ExprCoreType; +import org.opensearch.sql.data.type.ExprType; +import org.opensearch.sql.exception.ExpressionEvaluationException; +import org.opensearch.sql.expression.function.ImplementorUDF; +import org.opensearch.sql.expression.function.UDFOperandMetadata; + +/** + * Function that casts values to IP type. + * + *

Signature: + * + *

    + *
  • (STRING) -> IP + *
  • (IP) -> IP + *
+ */ +public class IPFunction extends ImplementorUDF { + + public IPFunction() { + super(new CastImplementor(), NullPolicy.ANY); + } + + @Override + public UDFOperandMetadata getOperandMetadata() { + return UDFOperandMetadata.wrapUDT( + List.of(List.of(ExprCoreType.IP), List.of(ExprCoreType.STRING))); + } + + @Override + public SqlReturnTypeInference getReturnTypeInference() { + return ReturnTypes.explicit( + OpenSearchTypeFactory.TYPE_FACTORY.createUDT(OpenSearchTypeFactory.ExprUDT.EXPR_IP, true)); + } + + public static class CastImplementor + implements org.apache.calcite.adapter.enumerable.NotNullImplementor { + @Override + public Expression implement( + RexToLixTranslator translator, RexCall call, List translatedOperands) { + if (call.getOperands().size() != 1) { + throw new IllegalArgumentException("IP function requires exactly one operand"); + } + ExprType argType = + OpenSearchTypeFactory.convertRelDataTypeToExprType( + call.getOperands().getFirst().getType()); + if (argType == ExprCoreType.IP) { + return translatedOperands.getFirst(); + } else if (argType == ExprCoreType.STRING) { + return Expressions.new_(ExprIpValue.class, translatedOperands); + } else { + throw new ExpressionEvaluationException( + String.format( + Locale.ROOT, + "Cannot convert %s to IP, only STRING and IP types are supported", + argType)); + } + } + } +} diff --git a/core/src/test/java/org/opensearch/sql/data/model/ExprIpValueTest.java b/core/src/test/java/org/opensearch/sql/data/model/ExprIpValueTest.java index b0ef598a5ae..9867df355de 100644 --- a/core/src/test/java/org/opensearch/sql/data/model/ExprIpValueTest.java +++ b/core/src/test/java/org/opensearch/sql/data/model/ExprIpValueTest.java @@ -122,12 +122,8 @@ public void testEquals() { @Test public void testToString() { - ipv4EqualStrings.forEach( - (s) -> - assertEquals(String.format("IP %s", ipv4String), ExprValueUtils.ipValue(s).toString())); - ipv6EqualStrings.forEach( - (s) -> - assertEquals(String.format("IP %s", ipv6String), ExprValueUtils.ipValue(s).toString())); + ipv4EqualStrings.forEach((s) -> assertEquals(ipv4String, ExprValueUtils.ipValue(s).toString())); + ipv6EqualStrings.forEach((s) -> assertEquals(ipv6String, ExprValueUtils.ipValue(s).toString())); } @Test diff --git a/docs/user/ppl/functions/conversion.rst b/docs/user/ppl/functions/conversion.rst index 31fb3e3cdf0..dbe4403540c 100644 --- a/docs/user/ppl/functions/conversion.rst +++ b/docs/user/ppl/functions/conversion.rst @@ -16,24 +16,29 @@ Description Usage: cast(expr as dateType) cast the expr to dataType. return the value of dataType. The following conversion rules are used: -+------------+--------+--------+---------+-------------+--------+--------+ -| Src/Target | STRING | NUMBER | BOOLEAN | TIMESTAMP | DATE | TIME | -+------------+--------+--------+---------+-------------+--------+--------+ -| STRING | | Note1 | Note1 | TIMESTAMP() | DATE() | TIME() | -+------------+--------+--------+---------+-------------+--------+--------+ -| NUMBER | Note1 | | v!=0 | N/A | N/A | N/A | -+------------+--------+--------+---------+-------------+--------+--------+ -| BOOLEAN | Note1 | v?1:0 | | N/A | N/A | N/A | -+------------+--------+--------+---------+-------------+--------+--------+ -| TIMESTAMP | Note1 | N/A | N/A | | DATE() | TIME() | -+------------+--------+--------+---------+-------------+--------+--------+ -| DATE | Note1 | N/A | N/A | N/A | | N/A | -+------------+--------+--------+---------+-------------+--------+--------+ -| TIME | Note1 | N/A | N/A | N/A | N/A | | -+------------+--------+--------+---------+-------------+--------+--------+ ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| Src/Target | STRING | NUMBER | BOOLEAN | TIMESTAMP | DATE | TIME | IP | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| STRING | | Note1 | Note1 | TIMESTAMP() | DATE() | TIME() | IP() | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| NUMBER | Note1 | | v!=0 | N/A | N/A | N/A | N/A | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| BOOLEAN | Note1 | v?1:0 | | N/A | N/A | N/A | N/A | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| TIMESTAMP | Note1 | N/A | N/A | | DATE() | TIME() | N/A | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| DATE | Note1 | N/A | N/A | N/A | | N/A | N/A | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| TIME | Note1 | N/A | N/A | N/A | N/A | | N/A | ++------------+--------+--------+---------+-------------+--------+--------+--------+ +| IP | Note2 | N/A | N/A | N/A | N/A | N/A | | ++------------+--------+--------+---------+-------------+--------+--------+--------+ Note1: the conversion follow the JDK specification. +Note2: IP will be converted to its canonical representation. Canonical representation +for IPv6 is described in `RFC 5952 `_. + Cast to string example:: os> source=people | eval `cbool` = CAST(true as string), `cint` = CAST(1 as string), `cdate` = CAST(CAST('2012-08-07' as date) as string) | fields `cbool`, `cint`, `cdate` diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLCastFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLCastFunctionIT.java index 7c21932d819..bd08a3a5214 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLCastFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLCastFunctionIT.java @@ -7,9 +7,8 @@ import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATATYPE_NONNUMERIC; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATATYPE_NUMERIC; -import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE_FORMATS; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_STATE_COUNTRY; -import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_STATE_COUNTRY_WITH_NULL; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_WEBLOGS; import static org.opensearch.sql.util.MatcherUtils.rows; import static org.opensearch.sql.util.MatcherUtils.schema; import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; @@ -19,91 +18,15 @@ import java.io.IOException; import org.json.JSONObject; import org.junit.Test; -import org.opensearch.sql.common.antlr.SyntaxCheckException; import org.opensearch.sql.exception.ExpressionEvaluationException; -import org.opensearch.sql.ppl.PPLIntegTestCase; +import org.opensearch.sql.ppl.CastFunctionIT; -public class CalcitePPLCastFunctionIT extends PPLIntegTestCase { +public class CalcitePPLCastFunctionIT extends CastFunctionIT { @Override public void init() throws Exception { super.init(); enableCalcite(); disallowCalciteFallback(); - - loadIndex(Index.STATE_COUNTRY); - loadIndex(Index.STATE_COUNTRY_WITH_NULL); - loadIndex(Index.DATA_TYPE_NUMERIC); - loadIndex(Index.DATA_TYPE_NONNUMERIC); - loadIndex(Index.DATE_FORMATS); - } - - @Test - public void testCast() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(age as string) | fields a", TEST_INDEX_STATE_COUNTRY)); - - verifySchema(actual, schema("a", "string")); - - verifyDataRows(actual, rows("70"), rows("30"), rows("25"), rows("20")); - } - - @Test - public void testCastOverriding() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval age = cast(age as STRING) | fields age", - TEST_INDEX_STATE_COUNTRY)); - - verifySchema(actual, schema("age", "string")); - - verifyDataRows(actual, rows("70"), rows("30"), rows("25"), rows("20")); - } - - @Test - public void testCastToUnknownType() { - assertThrowsWithReplace( - SyntaxCheckException.class, - () -> - executeQuery( - String.format( - "source=%s | eval age = cast(age as VARCHAR) | fields age", - TEST_INDEX_STATE_COUNTRY))); - } - - @Test - public void testChainedCast() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval age = cast(concat(cast(age as string), '0') as DOUBLE) | fields" - + " age", - TEST_INDEX_STATE_COUNTRY)); - - verifySchema(actual, schema("age", "double")); - - verifyDataRows(actual, rows(700.0), rows(300.0), rows(250.0), rows(200.0)); - } - - @Test - public void testCastNullValues() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(state as string) | fields a", - TEST_INDEX_STATE_COUNTRY_WITH_NULL)); - - verifySchema(actual, schema("a", "string")); - verifyDataRows( - actual, - rows("California"), - rows("New York"), - rows("Ontario"), - rows("Quebec"), - rows((Object) null), - rows((Object) null)); } @Test @@ -196,137 +119,6 @@ public void testCastLiteralToBoolean() throws IOException { verifyDataRows(actual, rows((Object) null)); } - @Test - public void testCastINT() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(integer_number as INTEGER) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(2)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(integer_number as FLOAT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(2)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(integer_number as LONG) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(2)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(integer_number as DOUBLE) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(2)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(integer_number as STRING) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows("2")); - } - - @Test - public void testCastLONG() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(long_number as INT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(1)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(long_number as FLOAT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(1)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(long_number as DOUBLE) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(1)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(long_number as STRING) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows("1")); - } - - @Test - public void testCastFLOAT() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(float_number as INT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(6)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(float_number as LONG) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(6)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(float_number as DOUBLE) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(6.199999809265137)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(float_number as STRING) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows("6.2")); - } - - @Test - public void testCastDOUBLE() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(double_number as INT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(5)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(double_number as LONG) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(5)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(double_number as FLOAT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(5.1)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(double_number as STRING) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows("5.1")); - } - @Test public void testCastBOOLEAN() throws IOException { JSONObject actual; @@ -374,150 +166,35 @@ public void testCastBOOLEAN() throws IOException { } @Test - public void testCastNumericSTRING() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast(cast(integer_number as STRING) as INT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(2)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(cast(long_number as STRING) as LONG) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(1)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(cast(float_number as STRING) as FLOAT) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(6.2)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(cast(double_number as STRING) as DOUBLE) | fields a", - TEST_INDEX_DATATYPE_NUMERIC)); - verifyDataRows(actual, rows(5.1)); - - actual = - executeQuery( - String.format( - "source=%s | eval a = cast(cast(boolean_value as STRING) as BOOLEAN)| fields a", - TEST_INDEX_DATATYPE_NONNUMERIC)); - verifyDataRows(actual, rows(true)); - } - - @Test - public void testCastDate() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast('1984-04-12' as DATE) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "date")); - verifyDataRows(actual, rows("1984-04-12"), rows("1984-04-12")); - - actual = - executeQuery( - String.format( - "source=%s | head 1 | eval a = cast('2023-10-01 12:00:00' as date) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "date")); - verifyDataRows(actual, rows("2023-10-01")); - + public void testCastIntegerToIp() { Throwable t = assertThrowsWithReplace( ExpressionEvaluationException.class, () -> executeQuery( String.format( - "source=%s | eval a = cast('09:07:42' as DATE) | fields a", - TEST_INDEX_DATE_FORMATS))); - - verifyErrorMessageContains(t, "date:09:07:42 in unsupported format, please use 'yyyy-MM-dd'"); - } - - @Test - public void testCastTime() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast('09:07:42' as TIME) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "time")); - verifyDataRows(actual, rows("09:07:42"), rows("09:07:42")); - - actual = - executeQuery( - String.format( - "source=%s | head 1 | eval a = cast('09:07:42.12345' as TIME) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "time")); - verifyDataRows(actual, rows("09:07:42.12345")); - - actual = - executeQuery( - String.format( - "source=%s | head 1 | eval a = cast('1985-10-09 12:00:00' as time) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "time")); - verifyDataRows(actual, rows("12:00:00")); - - Throwable t = - assertThrowsWithReplace( - ExpressionEvaluationException.class, - () -> - executeQuery( - String.format( - "source=%s | eval a = cast('1984-04-12' as TIME) | fields a", - TEST_INDEX_DATE_FORMATS))); - + "source=%s | eval a = cast(1 as ip) | fields a", + TEST_INDEX_DATATYPE_NUMERIC))); verifyErrorMessageContains( - t, "time:1984-04-12 in unsupported format, please use 'HH:mm:ss[.SSSSSSSSS]'"); + t, "Cannot convert INTEGER to IP, only STRING and IP types are supported"); } + // Not available in v2 @Test - public void testCastTimestamp() throws IOException { - JSONObject actual = - executeQuery( - String.format( - "source=%s | eval a = cast('1984-04-12 09:07:42' as TIMESTAMP) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "timestamp")); - verifyDataRows(actual, rows("1984-04-12 09:07:42"), rows("1984-04-12 09:07:42")); - - actual = - executeQuery( - String.format( - "source=%s | head 1 | eval a = cast('2023-10-01 12:00:00.123456' as timestamp) |" - + " fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "timestamp")); - verifyDataRows(actual, rows("2023-10-01 12:00:00.123456")); - - actual = + public void testCastIpToString() throws IOException { + // Test casting ip to string + var actual = executeQuery( String.format( - "source=%s | eval a = cast('1984-04-12' as TIMESTAMP) | fields a", - TEST_INDEX_DATE_FORMATS)); - verifySchema(actual, schema("a", "timestamp")); - verifyDataRows(actual, rows("1984-04-12 00:00:00"), rows("1984-04-12 00:00:00")); - - Throwable t = - assertThrowsWithReplace( - ExpressionEvaluationException.class, - () -> - executeQuery( - String.format( - "source=%s | eval a = cast('09:07:42' as TIMESTAMP) | fields a", - TEST_INDEX_DATE_FORMATS))); - - verifyErrorMessageContains( - t, - "timestamp:09:07:42 in unsupported format, please use 'yyyy-MM-dd HH:mm:ss[.SSSSSSSSS]'"); + "source=%s | eval s = cast(host as STRING) | fields s", TEST_INDEX_WEBLOGS)); + verifySchema(actual, schema("s", "string")); + verifyDataRows( + actual, + rows("::1"), + rows("0.0.0.2"), + rows("::3"), + rows("1.2.3.4"), + rows("1.2.3.5"), + rows("::ffff:1234")); } } diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/CastFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/CastFunctionIT.java new file mode 100644 index 00000000000..a95dedc699f --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/CastFunctionIT.java @@ -0,0 +1,435 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.ppl; + +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATATYPE_NONNUMERIC; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATATYPE_NUMERIC; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DATE_FORMATS; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_STATE_COUNTRY; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_STATE_COUNTRY_WITH_NULL; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_WEBLOGS; +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.verifyErrorMessageContains; +import static org.opensearch.sql.util.MatcherUtils.verifySchema; + +import java.io.IOException; +import org.json.JSONObject; +import org.junit.Test; +import org.opensearch.sql.common.antlr.SyntaxCheckException; +import org.opensearch.sql.exception.ExpressionEvaluationException; +import org.opensearch.sql.exception.SemanticCheckException; + +public class CastFunctionIT extends PPLIntegTestCase { + @Override + public void init() throws Exception { + loadIndex(Index.STATE_COUNTRY); + loadIndex(Index.STATE_COUNTRY_WITH_NULL); + loadIndex(Index.DATA_TYPE_NUMERIC); + loadIndex(Index.DATA_TYPE_NONNUMERIC); + loadIndex(Index.DATE_FORMATS); + loadIndex(Index.WEBLOG); + } + + @Test + public void testCast() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(age as string) | fields a", TEST_INDEX_STATE_COUNTRY)); + + verifySchema(actual, schema("a", "string")); + + verifyDataRows(actual, rows("70"), rows("30"), rows("25"), rows("20")); + } + + @Test + public void testCastOverriding() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval age = cast(age as STRING) | fields age", + TEST_INDEX_STATE_COUNTRY)); + + verifySchema(actual, schema("age", "string")); + + verifyDataRows(actual, rows("70"), rows("30"), rows("25"), rows("20")); + } + + @Test + public void testCastToUnknownType() { + assertThrowsWithReplace( + SyntaxCheckException.class, + () -> + executeQuery( + String.format( + "source=%s | eval age = cast(age as VARCHAR) | fields age", + TEST_INDEX_STATE_COUNTRY))); + } + + @Test + public void testChainedCast() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval age = cast(concat(cast(age as string), '0') as DOUBLE) | fields" + + " age", + TEST_INDEX_STATE_COUNTRY)); + + verifySchema(actual, schema("age", "double")); + + verifyDataRows(actual, rows(700.0), rows(300.0), rows(250.0), rows(200.0)); + } + + @Test + public void testCastNullValues() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(state as string) | fields a", + TEST_INDEX_STATE_COUNTRY_WITH_NULL)); + + verifySchema(actual, schema("a", "string")); + verifyDataRows( + actual, + rows("California"), + rows("New York"), + rows("Ontario"), + rows("Quebec"), + rows((Object) null), + rows((Object) null)); + } + + @Test + public void testCastINT() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(integer_number as INTEGER) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(2)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(integer_number as FLOAT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(2)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(integer_number as LONG) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(2)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(integer_number as DOUBLE) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(2)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(integer_number as STRING) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows("2")); + } + + @Test + public void testCastLONG() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(long_number as INT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(1)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(long_number as FLOAT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(1)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(long_number as DOUBLE) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(1)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(long_number as STRING) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows("1")); + } + + @Test + public void testCastFLOAT() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(float_number as INT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(6)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(float_number as LONG) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(6)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(float_number as DOUBLE) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(6.199999809265137)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(float_number as STRING) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows("6.2")); + } + + @Test + public void testCastDOUBLE() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(double_number as INT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(5)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(double_number as LONG) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(5)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(double_number as FLOAT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(5.1)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(double_number as STRING) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows("5.1")); + } + + @Test + public void testCastNumericSTRING() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast(cast(integer_number as STRING) as INT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(2)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(cast(long_number as STRING) as LONG) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(1)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(cast(float_number as STRING) as FLOAT) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(6.2)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(cast(double_number as STRING) as DOUBLE) | fields a", + TEST_INDEX_DATATYPE_NUMERIC)); + verifyDataRows(actual, rows(5.1)); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast(cast(boolean_value as STRING) as BOOLEAN)| fields a", + TEST_INDEX_DATATYPE_NONNUMERIC)); + verifyDataRows(actual, rows(true)); + } + + @Test + public void testCastDate() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast('1984-04-12' as DATE) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "date")); + verifyDataRows(actual, rows("1984-04-12"), rows("1984-04-12")); + + actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('2023-10-01 12:00:00' as date) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "date")); + verifyDataRows(actual, rows("2023-10-01")); + + Throwable t = + assertThrowsWithReplace( + ExpressionEvaluationException.class, + () -> + executeQuery( + String.format( + "source=%s | eval a = cast('09:07:42' as DATE) | fields a", + TEST_INDEX_DATE_FORMATS))); + + verifyErrorMessageContains(t, "date:09:07:42 in unsupported format, please use 'yyyy-MM-dd'"); + } + + @Test + public void testCastTime() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast('09:07:42' as TIME) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "time")); + verifyDataRows(actual, rows("09:07:42"), rows("09:07:42")); + + actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('09:07:42.12345' as TIME) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "time")); + verifyDataRows(actual, rows("09:07:42.12345")); + + actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('1985-10-09 12:00:00' as time) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "time")); + verifyDataRows(actual, rows("12:00:00")); + + Throwable t = + assertThrowsWithReplace( + ExpressionEvaluationException.class, + () -> + executeQuery( + String.format( + "source=%s | eval a = cast('1984-04-12' as TIME) | fields a", + TEST_INDEX_DATE_FORMATS))); + + verifyErrorMessageContains( + t, "time:1984-04-12 in unsupported format, please use 'HH:mm:ss[.SSSSSSSSS]'"); + } + + @Test + public void testCastTimestamp() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval a = cast('1984-04-12 09:07:42' as TIMESTAMP) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "timestamp")); + verifyDataRows(actual, rows("1984-04-12 09:07:42"), rows("1984-04-12 09:07:42")); + + actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('2023-10-01 12:00:00.123456' as timestamp) |" + + " fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "timestamp")); + verifyDataRows(actual, rows("2023-10-01 12:00:00.123456")); + + actual = + executeQuery( + String.format( + "source=%s | eval a = cast('1984-04-12' as TIMESTAMP) | fields a", + TEST_INDEX_DATE_FORMATS)); + verifySchema(actual, schema("a", "timestamp")); + verifyDataRows(actual, rows("1984-04-12 00:00:00"), rows("1984-04-12 00:00:00")); + + Throwable t = + assertThrowsWithReplace( + ExpressionEvaluationException.class, + () -> + executeQuery( + String.format( + "source=%s | eval a = cast('09:07:42' as TIMESTAMP) | fields a", + TEST_INDEX_DATE_FORMATS))); + + verifyErrorMessageContains( + t, + "timestamp:09:07:42 in unsupported format, please use 'yyyy-MM-dd HH:mm:ss[.SSSSSSSSS]'"); + } + + @Test + public void testCastToIP() throws IOException { + // Test casting IP to IP type + JSONObject actual = + executeQuery( + String.format("source=%s | eval a = cast(host as IP) | fields a", TEST_INDEX_WEBLOGS)); + verifySchema(actual, schema("a", "ip")); + verifyDataRows( + actual, + rows("::1"), + rows("0.0.0.2"), + rows("::3"), + rows("1.2.3.4"), + rows("1.2.3.5"), + rows("::ffff:1234")); + + // Test casting valid IP literal to IP type + actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('192.168.1.1' as IP) | fields a", + TEST_INDEX_WEBLOGS)); + verifySchema(actual, schema("a", "ip")); + verifyDataRows(actual, rows("192.168.1.1")); + + // Test casting IPv6 literal to IP type + actual = + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('2001:0db8:85a3:0000:0000:8a2e:0370:7334' as" + + " IP) | fields a", + TEST_INDEX_WEBLOGS)); + verifySchema(actual, schema("a", "ip")); + verifyDataRows(actual, rows("2001:db8:85a3::8a2e:370:7334")); + + // Test casting invalid IP string to IP type + Throwable t = + assertThrowsWithReplace( + SemanticCheckException.class, + () -> + executeQuery( + String.format( + "source=%s | head 1 | eval a = cast('invalid_ip' as IP) | fields a", + TEST_INDEX_WEBLOGS))); + verifyErrorMessageContains( + t, + "IP address string 'invalid_ip' is not valid. Error details: invalid_ip IP Address error:" + + " validation options do not allow you to specify a non-segmented single value"); + } +} diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLFunctionTypeTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLFunctionTypeTest.java index ef1237fa2ec..b52de9448ed 100644 --- a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLFunctionTypeTest.java +++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLFunctionTypeTest.java @@ -50,7 +50,10 @@ public void testComparisonWithDifferentType() { Throwable t = Assert.assertThrows(ExpressionEvaluationException.class, () -> getRelNode(ppl)); verifyErrorMessageContains( t, - "LESS function expects {[COMPARABLE_TYPE,COMPARABLE_TYPE]}," + " but got [STRING,INTEGER]"); + // Temporary fix for the error message as LESS function has two variants. Will remove + // [IP,IP],[IP,STRING],[STRING,IP] when merging the two variants. + "LESS function expects {[IP,IP],[IP,STRING],[STRING,IP],[COMPARABLE_TYPE,COMPARABLE_TYPE]}," + + " but got [STRING,INTEGER]"); } @Test