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 9c15c1485c1..b8ed9b6cd7e 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 @@ -47,7 +47,9 @@ import org.apache.calcite.sql.type.SqlTypeUtil; import org.checkerframework.checker.nullness.qual.Nullable; import org.opensearch.analytics.schema.BinaryType; +import org.opensearch.analytics.schema.DateOnlyType; import org.opensearch.analytics.schema.IpType; +import org.opensearch.analytics.schema.TimeOnlyType; import org.opensearch.sql.calcite.type.AbstractExprRelDataType; import org.opensearch.sql.calcite.type.ExprBinaryType; import org.opensearch.sql.calcite.type.ExprDateType; @@ -277,6 +279,16 @@ public static ExprType convertRelDataTypeToExprType(RelDataType type) { return exprType; } + /** DATE check for return-type inference; recognizes the analytics-route {@link DateOnlyType}. */ + public static boolean isDateExprType(RelDataType type) { + return type instanceof DateOnlyType || convertRelDataTypeToExprType(type) == ExprCoreType.DATE; + } + + /** TIME counterpart of {@link #isDateExprType}; recognizes {@link TimeOnlyType}. */ + public static boolean isTimeExprType(RelDataType type) { + return type instanceof TimeOnlyType || convertRelDataTypeToExprType(type) == ExprCoreType.TIME; + } + /** * Result-schema-only variant of {@link #convertRelDataTypeToExprType} that recognizes the * analytics-engine {@link IpType} / {@link BinaryType} markers as {@link ExprCoreType#IP} / @@ -293,6 +305,13 @@ public static ExprType convertAnalyticsEngineRelDataTypeToExprType(RelDataType t if (type instanceof BinaryType) { return BINARY; } + // span() over date / time UDT — schema label is DATE / TIME, not TIMESTAMP. + if (type instanceof DateOnlyType) { + return DATE; + } + if (type instanceof TimeOnlyType) { + return TIME; + } return convertRelDataTypeToExprType(type); } diff --git a/core/src/main/java/org/opensearch/sql/calcite/utils/PPLReturnTypes.java b/core/src/main/java/org/opensearch/sql/calcite/utils/PPLReturnTypes.java index acf2a6b8ebc..ce7dd408e7a 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/utils/PPLReturnTypes.java +++ b/core/src/main/java/org/opensearch/sql/calcite/utils/PPLReturnTypes.java @@ -12,7 +12,6 @@ import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.type.SqlTypeTransforms; import org.apache.calcite.sql.type.SqlTypeUtil; -import org.opensearch.sql.data.type.ExprCoreType; /** * Return types used in PPL. This class complements the {@link @@ -36,8 +35,7 @@ private PPLReturnTypes() {} public static SqlReturnTypeInference TIME_APPLY_RETURN_TYPE = opBinding -> { RelDataType temporalType = opBinding.getOperandType(0); - if (ExprCoreType.TIME.equals( - OpenSearchTypeFactory.convertRelDataTypeToExprType(temporalType))) { + if (OpenSearchTypeFactory.isTimeExprType(temporalType)) { return UserDefinedFunctionUtils.NULLABLE_TIME_UDT; } return UserDefinedFunctionUtils.NULLABLE_TIMESTAMP_UDT; diff --git a/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java b/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java index 733c603a761..68dffacc2eb 100644 --- a/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java +++ b/core/src/main/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngine.java @@ -12,6 +12,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import org.apache.calcite.plan.RelOptUtil; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.type.RelDataType; @@ -19,7 +20,9 @@ import org.opensearch.analytics.exec.QueryPlanExecutor; import org.opensearch.analytics.exec.profile.ProfiledResult; import org.opensearch.analytics.schema.BinaryType; +import org.opensearch.analytics.schema.DateOnlyType; import org.opensearch.analytics.schema.IpType; +import org.opensearch.analytics.schema.TimeOnlyType; import org.opensearch.common.network.InetAddresses; import org.opensearch.core.action.ActionListener; import org.opensearch.sql.ast.statement.ExplainMode; @@ -47,6 +50,15 @@ */ public class AnalyticsExecutionEngine implements ExecutionEngine { + // TIME-typed list elements round-trip via Timestamp and bypass ArrowValues' scalar + // post-processing (see DataFusion list_merge), arriving as "1970-01-01[ T]HH:mm:ss[.frac]". + private static final Pattern EPOCH_DATE_TIME_PREFIX = + Pattern.compile("^1970-01-01[ T](\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)$"); + + // DateOnlyType wire is Timestamp(ms) at midnight — keep the date, drop the time. + private static final Pattern DATE_WITH_MIDNIGHT_TIME = + Pattern.compile("^(\\d{4}-\\d{2}-\\d{2})[ T]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?$"); + private final QueryPlanExecutor> planExecutor; public AnalyticsExecutionEngine(QueryPlanExecutor> planExecutor) { @@ -206,21 +218,7 @@ private List convertRows(Iterable rows, List - *
  • {@link IpType} + {@code byte[]} → canonical address string (matches {@code - * IpFieldMapper}'s {@code valueFetcher} output). - *
  • {@link BinaryType} + {@code byte[]} → base64-encoded string (matches the OpenSearch - * {@code binary} field wire format). - *
  • Anything else → existing {@link ExprValueUtils#fromObjectValue} path. - * - * - *

    Without this dispatch, {@code fromObjectValue} throws {@code unsupported object class [B} on - * byte[] cells, and IP buffers leak through as raw 16-byte ipv4-mapped-ipv6 garbage. - */ + /** Renders UDT cells (IP/binary byte[]; date / time string) and strips TIME prefixes in lists. */ private static ExprValue toExprValue(Object value, RelDataType type) { if (value instanceof byte[] bytes) { if (type instanceof IpType) { @@ -234,9 +232,39 @@ private static ExprValue toExprValue(Object value, RelDataType type) { return ExprValueUtils.stringValue(Base64.getEncoder().encodeToString(bytes)); } } + // DateOnlyType scalar — strip midnight time suffix off the Timestamp(ms) wire. + if (type instanceof DateOnlyType && value instanceof String s) { + var m = DATE_WITH_MIDNIGHT_TIME.matcher(s); + if (m.matches()) { + return ExprValueUtils.stringValue(m.group(1)); + } + } + // TimeOnlyType scalar — strip 1970-01-01 prefix off the Timestamp(ms) wire. + if (type instanceof TimeOnlyType && value instanceof String s) { + var m = EPOCH_DATE_TIME_PREFIX.matcher(s); + if (m.matches()) { + return ExprValueUtils.stringValue(m.group(1)); + } + } + if (value instanceof List list) { + return ExprValueUtils.collectionValue(stripEpochDatePrefixInList(list)); + } return ExprValueUtils.fromObjectValue(value); } + private static List stripEpochDatePrefixInList(List list) { + List out = new ArrayList<>(list.size()); + for (Object element : list) { + if (element instanceof String s) { + var m = EPOCH_DATE_TIME_PREFIX.matcher(s); + out.add(m.matches() ? m.group(1) : s); + } else { + out.add(element); + } + } + return out; + } + private Schema buildSchema(List fields) { List columns = new ArrayList<>(); for (RelDataTypeField field : fields) { diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/datetime/AddSubDateFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/datetime/AddSubDateFunction.java index 9456e6b857a..2c040918e80 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/udf/datetime/AddSubDateFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/datetime/AddSubDateFunction.java @@ -63,7 +63,7 @@ public SqlReturnTypeInference getReturnTypeInference() { return opBinding -> { RelDataType temporalType = opBinding.getOperandType(0); RelDataType temporalDeltaType = opBinding.getOperandType(1); - if (OpenSearchTypeFactory.convertRelDataTypeToExprType(temporalType) == ExprCoreType.DATE + if (OpenSearchTypeFactory.isDateExprType(temporalType) && SqlTypeFamily.NUMERIC.contains(temporalDeltaType)) { return NULLABLE_DATE_UDT; } else { diff --git a/core/src/test/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngineTest.java b/core/src/test/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngineTest.java index e759b128a45..08b88048468 100644 --- a/core/src/test/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngineTest.java +++ b/core/src/test/java/org/opensearch/sql/executor/analytics/AnalyticsExecutionEngineTest.java @@ -30,7 +30,9 @@ import org.junit.jupiter.api.Test; import org.opensearch.analytics.exec.QueryPlanExecutor; import org.opensearch.analytics.schema.BinaryType; +import org.opensearch.analytics.schema.DateOnlyType; import org.opensearch.analytics.schema.IpType; +import org.opensearch.analytics.schema.TimeOnlyType; import org.opensearch.core.action.ActionListener; import org.opensearch.sql.calcite.CalcitePlanContext; import org.opensearch.sql.calcite.SysLimit; @@ -237,6 +239,67 @@ void executeRelNode_binaryColumnRendersAsBase64() { "byte[] should base64-encode to match OpenSearch binary wire format. " + dump); } + /** DateOnlyType — schema reports DATE, value strips midnight suffix. */ + @Test + void executeRelNode_dateOnlyTypeStripsTimeSuffix() { + RelNode relNode = + mockRelNodeWithType("d", new DateOnlyType(RelDataTypeSystem.DEFAULT, true, 3)); + Iterable rows = Collections.singletonList(new Object[] {"1984-04-12 00:00:00"}); + stubExecutorWith(relNode, rows); + + QueryResponse response = executeAndCapture(relNode); + String dump = dumpResponse(response); + + assertEquals(ExprCoreType.DATE, response.getSchema().getColumns().get(0).getExprType(), dump); + assertEquals( + "1984-04-12", response.getResults().get(0).tupleValue().get("d").stringValue(), dump); + } + + /** TimeOnlyType — schema reports TIME, value strips 1970-01-01 prefix. */ + @Test + void executeRelNode_timeOnlyTypeStripsEpochDatePrefix() { + RelNode relNode = + mockRelNodeWithType("t", new TimeOnlyType(RelDataTypeSystem.DEFAULT, true, 3)); + Iterable rows = Collections.singletonList(new Object[] {"1970-01-01 09:00:00"}); + stubExecutorWith(relNode, rows); + + QueryResponse response = executeAndCapture(relNode); + String dump = dumpResponse(response); + + assertEquals(ExprCoreType.TIME, response.getSchema().getColumns().get(0).getExprType(), dump); + assertEquals( + "09:00:00", response.getResults().get(0).tupleValue().get("t").stringValue(), dump); + } + + /** TIME-typed list elements arrive as "1970-01-01[ T]HH:mm:ss[.frac]" — strip the prefix. */ + @Test + void executeRelNode_listOfStringStripsEpochDatePrefix() { + SqlTypeFactoryImpl typeFactory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT); + RelDataType arrayOfVarchar = + typeFactory.createArrayType(typeFactory.createSqlType(SqlTypeName.VARCHAR), -1); + RelNode relNode = mockRelNodeWithType("time_list", arrayOfVarchar); + java.util.List input = + Arrays.asList( + "1970-01-01 19:36:22", + "1970-01-01T02:05:25", + "1970-01-01 12:34:56.123456789", + "2020-10-13 13:00:00", + "hello"); + stubExecutorWith(relNode, Collections.singletonList(new Object[] {input})); + + QueryResponse response = executeAndCapture(relNode); + String dump = dumpResponse(response); + + java.util.List result = + response.getResults().get(0).tupleValue().get("time_list").collectionValue().stream() + .map(org.opensearch.sql.data.model.ExprValue::stringValue) + .toList(); + assertEquals( + Arrays.asList("19:36:22", "02:05:25", "12:34:56.123456789", "2020-10-13 13:00:00", "hello"), + result, + dump); + } + @Test void executeRelNode_emptyResults() { RelNode relNode = mockRelNode("name", SqlTypeName.VARCHAR);