Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.DateType;
import org.opensearch.analytics.schema.IpType;
import org.opensearch.analytics.schema.TimeType;
import org.opensearch.sql.calcite.type.AbstractExprRelDataType;
import org.opensearch.sql.calcite.type.ExprBinaryType;
import org.opensearch.sql.calcite.type.ExprDateType;
Expand Down Expand Up @@ -293,6 +295,12 @@ public static ExprType convertAnalyticsEngineRelDataTypeToExprType(RelDataType t
if (type instanceof BinaryType) {
return BINARY;
}
if (type instanceof DateType) {
return DATE;
}
if (type instanceof TimeType) {
return TIME;
}
return convertRelDataTypeToExprType(type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
Expand All @@ -19,7 +24,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.DateType;
import org.opensearch.analytics.schema.IpType;
import org.opensearch.analytics.schema.TimeType;
import org.opensearch.common.network.InetAddresses;
import org.opensearch.core.action.ActionListener;
import org.opensearch.sql.ast.statement.ExplainMode;
Expand Down Expand Up @@ -234,9 +241,87 @@ private static ExprValue toExprValue(Object value, RelDataType type) {
return ExprValueUtils.stringValue(Base64.getEncoder().encodeToString(bytes));
}
}
// A date-only column is stored (and arrives) as an epoch-millisecond timestamp; render it as a
// SQL DATE so it matches the v2 / Calcite path (1984-04-12 rather than 1984-04-12 00:00:00).
if (type instanceof DateType) {
LocalDate date = toLocalDate(value);
if (date != null) {
return ExprValueUtils.fromObjectValue(date);
}
}
// A time-only column likewise arrives as an epoch-millisecond timestamp (on 1970-01-01); render
// it as a SQL TIME so it matches the v2 / Calcite path (09:00:00 rather than 1970-01-01
// 09:00:00).
if (type instanceof TimeType) {
LocalTime time = toLocalTime(value);
if (time != null) {
return ExprValueUtils.fromObjectValue(time);
}
}
return ExprValueUtils.fromObjectValue(value);
}

/**
* Truncates a timestamp-shaped result cell to its {@link LocalDate}. Returns {@code null} for a
* {@code null} cell or an unrecognized runtime type so the caller falls back to the default
* conversion. Epoch-based values are interpreted at UTC, matching how OpenSearch stores dates.
*/
private static LocalDate toLocalDate(Object value) {
if (value == null) {
return null;
}
if (value instanceof LocalDate localDate) {
return localDate;
}
if (value instanceof LocalDateTime localDateTime) {
return localDateTime.toLocalDate();
}
if (value instanceof Instant instant) {
return instant.atZone(ZoneOffset.UTC).toLocalDate();
}
if (value instanceof java.sql.Timestamp timestamp) {
return timestamp.toInstant().atZone(ZoneOffset.UTC).toLocalDate();
}
if (value instanceof java.sql.Date date) {
return date.toLocalDate();
}
if (value instanceof Number number) {
return Instant.ofEpochMilli(number.longValue()).atZone(ZoneOffset.UTC).toLocalDate();
}
return null;
}

/**
* Extracts the {@link LocalTime} (time-of-day) from a timestamp-shaped result cell. Returns
* {@code null} for a {@code null} cell or an unrecognized runtime type so the caller falls back
* to the default conversion. Epoch-based values are interpreted at UTC, matching how OpenSearch
* stores times (as a timestamp on 1970-01-01).
*/
private static LocalTime toLocalTime(Object value) {
if (value == null) {
return null;
}
if (value instanceof LocalTime localTime) {
return localTime;
}
if (value instanceof LocalDateTime localDateTime) {
return localDateTime.toLocalTime();
}
if (value instanceof Instant instant) {
return instant.atZone(ZoneOffset.UTC).toLocalTime();
}
if (value instanceof java.sql.Timestamp timestamp) {
return timestamp.toInstant().atZone(ZoneOffset.UTC).toLocalTime();
}
if (value instanceof java.sql.Time time) {
return time.toLocalTime();
}
if (value instanceof Number number) {
return Instant.ofEpochMilli(number.longValue()).atZone(ZoneOffset.UTC).toLocalTime();
}
return null;
}

private Schema buildSchema(List<RelDataTypeField> fields) {
List<Schema.Column> columns = new ArrayList<>();
for (RelDataTypeField field : fields) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,28 +93,31 @@ public void testTimestampRootColumnSpaceFormat() throws IOException {
}

/**
* DATE-mapped col: AE widens to TIMESTAMP at scan time; value must use space separator, not ISO
* {@code T}.
* DATE-mapped col with a date-only format: the analytics engine surfaces it as a SQL {@code DATE}
* (via the {@code DateType} UDT), matching the v2 / Calcite path — date schema type and a
* date-only value, not a widened {@code TIMESTAMP}.
*/
@Test
public void testDateRootColumnYmdFormat() throws IOException {
String query = "source=" + INDEX + " | where d = '2024-03-15' | fields d";
assertRoutedToAnalyticsEngine(query);
JSONObject result = executeQuery(query);
verifySchema(result, schema("d", "timestamp"));
verifyDataRows(result, rows("2024-03-15 00:00:00"));
verifySchema(result, schema("d", "date"));
verifyDataRows(result, rows("2024-03-15"));
}

/** TIME-mapped col: AE widens to TIMESTAMP; value must use space separator, not ISO {@code T}. */
/**
* TIME-mapped col with a time-only format: the analytics engine surfaces it as a SQL {@code TIME}
* (via the {@code TimeType} UDT), matching the v2 / Calcite path — time schema type and a
* time-of-day value, not a widened {@code TIMESTAMP}.
*/
@Test
public void testTimeRootColumnHmsFormat() throws IOException {
String query = "source=" + INDEX + " | sort t | head 1 | fields t";
assertRoutedToAnalyticsEngine(query);
JSONObject result = executeQuery(query);
verifySchema(result, schema("t", "timestamp"));
Assert.assertFalse(
"Time-mapped column must not surface as ISO T-separator literal",
result.getJSONArray("datarows").getJSONArray(0).getString(0).contains("T"));
verifySchema(result, schema("t", "time"));
verifyDataRows(result, rows("10:30:00"));
}

/** Eval-derived TIMESTAMP follows the same wire-format contract as a root column. */
Expand Down
Loading