Skip to content
Closed
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.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;
Expand Down Expand Up @@ -293,6 +295,14 @@ public static ExprType convertAnalyticsEngineRelDataTypeToExprType(RelDataType t
if (type instanceof BinaryType) {
return BINARY;
}
// Sandbox UDT markers for format-classified date columns (Timestamp(ms)-backed wire,
// user-visible label downgraded to date / time).
if (type instanceof DateOnlyType) {
return DATE;
}
if (type instanceof TimeOnlyType) {
return TIME;
}
return convertRelDataTypeToExprType(type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
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;
import org.apache.calcite.rel.type.RelDataTypeField;
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;
Expand Down Expand Up @@ -47,6 +50,17 @@
*/
public class AnalyticsExecutionEngine implements ExecutionEngine {

// TIME-typed columns round-trip through Timestamp and arrive in list elements as
// "1970-01-01[ T]HH:mm:ss[.fraction]"; analytics-engine post-processes scalars but
// list-aggregation elements bypass that path (see list_merge in DataFusion).
private static final Pattern EPOCH_DATE_TIME_PREFIX =
Pattern.compile("^1970-01-01[ T](\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)$");

// DATE-typed columns whose wire is Timestamp(ms) arrive as "YYYY-MM-DD HH:mm:ss";
// when the column carries a DateOnlyType marker we strip the time suffix.
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<RelNode, Iterable<Object[]>> planExecutor;

public AnalyticsExecutionEngine(QueryPlanExecutor<RelNode, Iterable<Object[]>> planExecutor) {
Expand Down Expand Up @@ -234,9 +248,44 @@ private static ExprValue toExprValue(Object value, RelDataType type) {
return ExprValueUtils.stringValue(Base64.getEncoder().encodeToString(bytes));
}
}
// span(date-typed) returns Timestamp(ms) wire with midnight time; render as YYYY-MM-DD only.
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));
}
}
// span(time-typed) returns Timestamp(ms) wire with 1970-01-01 prefix; render as HH:mm:ss only.
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));
}
}
// List elements that look like a sentinel-epoch-prefixed time render as HH:mm:ss only.
if (value instanceof List<?> list) {
return ExprValueUtils.collectionValue(stripEpochDatePrefixInList(list));
}
return ExprValueUtils.fromObjectValue(value);
}

/**
* Returns a copy of {@code list} with each "1970-01-01[ T]HH:mm:ss[.fraction]" string replaced by
* the time portion only; non-matching elements pass through unchanged.
*/
private static List<Object> stripEpochDatePrefixInList(List<?> list) {
List<Object> 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<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 @@ -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;
Expand Down Expand Up @@ -237,6 +239,72 @@ void executeRelNode_binaryColumnRendersAsBase64() {
"byte[] should base64-encode to match OpenSearch binary wire format. " + dump);
}

/**
* DateOnlyType column value "YYYY-MM-DD HH:MM:SS" → schema "date" + value stripped to YYYY-MM-DD.
*/
@Test
void executeRelNode_dateOnlyTypeStripsTimeSuffix() {
RelNode relNode = mockRelNodeWithType("d", new DateOnlyType(RelDataTypeSystem.DEFAULT, true));
Iterable<Object[]> 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 column value "1970-01-01 HH:MM:SS" → schema "time" + value stripped to HH:MM:SS.
*/
@Test
void executeRelNode_timeOnlyTypeStripsEpochDatePrefix() {
RelNode relNode = mockRelNodeWithType("t", new TimeOnlyType(RelDataTypeSystem.DEFAULT, true));
Iterable<Object[]> 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 varchar = typeFactory.createSqlType(SqlTypeName.VARCHAR);
RelDataType arrayOfVarchar = typeFactory.createArrayType(varchar, -1);
RelDataType rowType = typeFactory.builder().add("time_list", arrayOfVarchar).build();
RelNode relNode = mock(RelNode.class);
when(relNode.getRowType()).thenReturn(rowType);
java.util.List<String> 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");
Iterable<Object[]> rows = Collections.singletonList(new Object[] {input});
stubExecutorWith(relNode, rows);

QueryResponse response = executeAndCapture(relNode);
String dump = dumpResponse(response);

java.util.List<String> 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);
Expand Down
Loading