Skip to content

Commit 47a27a7

Browse files
committed
Remove DatetimeOutputCastRule on the analytics-engine route (#5420)
On the analytics-engine route, every datetime root column is wrapped in `CAST(<DATE/TIME/TIMESTAMP> AS VARCHAR)` by `DatetimeOutputCastRule`. A matching `DatetimeOutputCastRewriter` on the engine side then translates that cast into DataFusion's `to_char` extension. Whenever the rewriter's format string and the PPL formatter disagree (e.g. trailing `Z`, `T` separator), users see wire-format divergence — issue #5420. Drop the cast rule and let the engine return real datetime cells. The PPL response pipeline already handles datetime → string conversion natively at the formatter layer: - `AnalyticsExecutionEngine.convertRows` feeds engine cells (`LocalDateTime` / `LocalDate` / `LocalTime`) through `ExprValueUtils.fromObjectValue`, which produces `ExprTimestampValue` / `ExprDateValue` / `ExprTimeValue`. - Their `value()` methods render the documented PPL space-separated format (`yyyy-MM-dd HH:mm:ss[.SSSSSSSSS]` etc.). The companion change in `opensearch-project/OpenSearch` removes the matching `DatetimeOutputCastRewriter` and its callsites. - Delete `DatetimeOutputCastRule`. - Drop the unused `UdtMapping.isDatetimeType` helper. - Rewrite the four cast-shape assertions in `DatetimeExtensionTest` to assert the post-removal RelNode (no `CAST(... VARCHAR)` wrapper, schema reports `DATE` / `TIME` / `TIMESTAMP`). Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
1 parent c4cac2a commit 47a27a7

5 files changed

Lines changed: 26 additions & 103 deletions

File tree

api/src/main/java/org/opensearch/sql/api/spec/datetime/DatetimeExtension.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
import org.opensearch.sql.calcite.type.AbstractExprRelDataType;
1818
import org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.ExprUDT;
1919

20-
/** Datetime language extension that normalizes UDT types and casts output for wire-format. */
20+
/** Datetime language extension that normalizes datetime UDT types to standard Calcite types. */
2121
public class DatetimeExtension implements LanguageExtension {
2222

2323
@Override
2424
public List<RelShuttle> postAnalysisRules() {
25-
// Fresh instances per plan() because RelHomogeneousShuttle inherits a stateful stack.
26-
return List.of(new DatetimeUdtNormalizeRule(), new DatetimeOutputCastRule());
25+
// Fresh instance per plan() because RelHomogeneousShuttle inherits a stateful stack.
26+
return List.of(new DatetimeUdtNormalizeRule());
2727
}
2828

2929
/** Maps datetime UDT types to their standard Calcite equivalents. */
@@ -45,10 +45,5 @@ static Optional<UdtMapping> fromUdtType(RelDataType type) {
4545
ExprUDT udt = e.getUdt();
4646
return Arrays.stream(values()).filter(u -> u.udtType == udt).findFirst();
4747
}
48-
49-
/** Returns true if the given SqlTypeName is a standard datetime type. */
50-
static boolean isDatetimeType(SqlTypeName typeName) {
51-
return Arrays.stream(values()).anyMatch(u -> u.stdType == typeName);
52-
}
5348
}
5449
}

api/src/main/java/org/opensearch/sql/api/spec/datetime/DatetimeOutputCastRule.java

Lines changed: 0 additions & 62 deletions
This file was deleted.

api/src/test/java/org/opensearch/sql/api/spec/datetime/DatetimeExtensionSqlTest.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
import org.opensearch.sql.executor.QueryType;
2121

2222
/**
23-
* Tests that DatetimeExtension post-analysis rules (UDT normalization and output VARCHAR cast)
24-
* apply correctly to the SQL V2 parser path through CalciteRelNodeVisitor.
23+
* Tests that the DatetimeExtension UDT-normalization post-analysis rule applies correctly to the
24+
* SQL V2 parser path through CalciteRelNodeVisitor.
2525
*/
2626
public class DatetimeExtensionSqlTest extends UnifiedQueryTestBase {
2727

@@ -57,12 +57,11 @@ private Table createEventsTable() {
5757
}
5858

5959
@Test
60-
public void testAllStandardDatetimeTypesCastToVarchar() {
60+
public void testStandardDatetimeTypesNotWrapped() {
6161
givenQuery("SELECT * FROM catalog.events")
6262
.assertPlan(
6363
"""
64-
LogicalProject(id=[$0], name=[$1], hire_date=[CAST($2):VARCHAR NOT NULL], start_time=[CAST($3):VARCHAR NOT NULL], created_at=[CAST($4):VARCHAR NOT NULL])
65-
LogicalTableScan(table=[[catalog, events]])
64+
LogicalTableScan(table=[[catalog, events]])
6665
""");
6766
}
6867

@@ -71,9 +70,8 @@ public void testFilterWithTimestampLiteral() {
7170
givenQuery("SELECT * FROM catalog.events WHERE created_at > '2024-01-01T00:00:00Z'")
7271
.assertPlan(
7372
"""
74-
LogicalProject(id=[$0], name=[$1], hire_date=[CAST($2):VARCHAR NOT NULL], start_time=[CAST($3):VARCHAR NOT NULL], created_at=[CAST($4):VARCHAR NOT NULL])
75-
LogicalFilter(condition=[>($4, TIMESTAMP('2024-01-01T00:00:00Z':VARCHAR))])
76-
LogicalTableScan(table=[[catalog, events]])
73+
LogicalFilter(condition=[>($4, TIMESTAMP('2024-01-01T00:00:00Z':VARCHAR))])
74+
LogicalTableScan(table=[[catalog, events]])
7775
""")
7876
.assertReturnType("TIMESTAMP", TIMESTAMP, 9);
7977
}
@@ -83,9 +81,8 @@ public void testComparisonWithDatetimeUdf() {
8381
givenQuery("SELECT * FROM catalog.events WHERE created_at < DATE(name)")
8482
.assertPlan(
8583
"""
86-
LogicalProject(id=[$0], name=[$1], hire_date=[CAST($2):VARCHAR NOT NULL], start_time=[CAST($3):VARCHAR NOT NULL], created_at=[CAST($4):VARCHAR NOT NULL])
87-
LogicalFilter(condition=[<($4, TIMESTAMP(DATE($1)))])
88-
LogicalTableScan(table=[[catalog, events]])
84+
LogicalFilter(condition=[<($4, TIMESTAMP(DATE($1)))])
85+
LogicalTableScan(table=[[catalog, events]])
8986
""")
9087
.assertReturnType("DATE", DATE)
9188
.assertReturnType("TIMESTAMP", TIMESTAMP, 9);

api/src/test/java/org/opensearch/sql/api/spec/datetime/DatetimeExtensionTest.java

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private Table createEventsTable() {
6363
}
6464

6565
@Test
66-
public void testUdfResultNormalizedAndCastToVarchar() {
66+
public void testUdfResultNormalized() {
6767
givenQuery(
6868
"""
6969
source = catalog.events \
@@ -72,9 +72,8 @@ public void testUdfResultNormalizedAndCastToVarchar() {
7272
""")
7373
.assertPlan(
7474
"""
75-
LogicalProject(d=[CAST($0):VARCHAR], t=[CAST($1):VARCHAR], ts=[CAST($2):VARCHAR])
76-
LogicalProject(d=[DATE($1)], t=[TIME($1)], ts=[TIMESTAMP($1)])
77-
LogicalTableScan(table=[[catalog, events]])
75+
LogicalProject(d=[DATE($1)], t=[TIME($1)], ts=[TIMESTAMP($1)])
76+
LogicalTableScan(table=[[catalog, events]])
7877
""")
7978
.assertReturnType("DATE", DATE)
8079
.assertReturnType("TIME", TIME, 9)
@@ -94,13 +93,12 @@ public void testNestedUdfCallsNormalized() {
9493
}
9594

9695
@Test
97-
public void testDateLiteralCastToVarchar() {
96+
public void testDateLiteralNormalized() {
9897
givenQuery("source = catalog.events | eval d = DATE('2024-01-01') | fields d")
9998
.assertPlan(
10099
"""
101-
LogicalProject(d=[CAST($0):VARCHAR])
102-
LogicalProject(d=[DATE('2024-01-01':VARCHAR)])
103-
LogicalTableScan(table=[[catalog, events]])
100+
LogicalProject(d=[DATE('2024-01-01':VARCHAR)])
101+
LogicalTableScan(table=[[catalog, events]])
104102
""")
105103
.assertReturnType("DATE", DATE);
106104
}
@@ -134,13 +132,12 @@ public void testComparisonWithDatetimeUdf() {
134132
}
135133

136134
@Test
137-
public void testAllStandardDatetimeTypesCastToVarchar() {
135+
public void testStandardDatetimeFieldsNotWrapped() {
138136
givenQuery("source = catalog.events | fields hire_date, start_time, created_at")
139137
.assertPlan(
140138
"""
141-
LogicalProject(hire_date=[CAST($0):VARCHAR NOT NULL], start_time=[CAST($1):VARCHAR NOT NULL], created_at=[CAST($2):VARCHAR NOT NULL])
142-
LogicalProject(hire_date=[$2], start_time=[$3], created_at=[$4])
143-
LogicalTableScan(table=[[catalog, events]])
139+
LogicalProject(hire_date=[$2], start_time=[$3], created_at=[$4])
140+
LogicalTableScan(table=[[catalog, events]])
144141
""");
145142
}
146143

@@ -176,19 +173,16 @@ public void testSequentialPlanCallsDoNotCorruptShuttleStack() {
176173
}
177174

178175
@Test
179-
public void testOutputCastCanCompileAndExecute() throws Exception {
176+
public void testDatetimeFieldsPreserveStandardTypes() throws Exception {
180177
RelNode plan =
181178
planner.plan("source = catalog.events | fields hire_date, start_time, created_at");
182179
try (PreparedStatement statement = compiler.compile(plan)) {
183180
ResultSet resultSet = statement.executeQuery();
184181
verify(resultSet)
185182
.expectSchema(
186-
col("hire_date", java.sql.Types.VARCHAR),
187-
col("start_time", java.sql.Types.VARCHAR),
188-
col("created_at", java.sql.Types.VARCHAR))
189-
.expectData(
190-
row("2024-01-16", "12:00:00", "2024-01-15 08:00:00"),
191-
row("2024-06-20", "14:00:00", "2024-06-20 00:00:00"));
183+
col("hire_date", java.sql.Types.DATE),
184+
col("start_time", java.sql.Types.TIME),
185+
col("created_at", java.sql.Types.TIMESTAMP));
192186
}
193187
}
194188
}

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePlannerConcurrencyIT.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@
3232
* <p>The test methods here fire many queries through a thread pool to exercise concurrent {@code
3333
* plan()} invocations. New planner-level concurrency / state-isolation regressions belong in this
3434
* class. The current cases cover {@code DatetimeExtension}'s {@code RelHomogeneousShuttle}
35-
* subclasses ({@code DatetimeUdtNormalizeRule}, {@code DatetimeOutputCastRule}) which were
36-
* previously returned as static {@code INSTANCE}s and caused the production failure that motivated
37-
* this suite.
35+
* subclass {@code DatetimeUdtNormalizeRule}, which was previously returned as a static {@code
36+
* INSTANCE} and caused the production failure that motivated this suite.
3837
*
3938
* <p>Run via:
4039
*

0 commit comments

Comments
 (0)