Skip to content

Commit 672aa21

Browse files
committed
[analytics-backend-datafusion] Preserve fractional seconds in DatetimeOutputCastRewriter format
Widen the format string passed to `to_char` from the seconds-only {@code "%Y-%m-%d %H:%M:%S"} to {@code "%Y-%m-%d %H:%M:%S%.f"}. The trailing {@code %.f} is chrono's variable-length fractional-second specifier — a leading dot followed by 0-9 digits, omitted when the value has no sub-second precision. This matches PPL's legacy formatting for {@code date} and {@code date_nanos} fields where the displayed precision tracks the source value: - {@code 2024-01-15T10:30:01.23456789Z} (date_nanos) → {@code "2024-01-15 10:30:01.23456789"} (legacy) / now {@code "2024-01-15 10:30:01.234567890"} (analytics route — internally 9-digit, leading 0 because Arrow Timestamp(ns) precision is fixed) - {@code 2025-08-01T03:47:41Z} (date) → {@code "2025-08-01 03:47:41"} (both paths) — no decimal because the source value has no fractional component Surfaced by `CalciteSearchCommandIT.testSearchWithDateRangeComparisons`. Follow-up to opensearch-project/sql#5420 which the original PR (#21650) closed with a seconds-only format that dropped the fractional digits the tests still expect. Signed-off-by: Kai Huang <ahkcs@amazon.com>
1 parent 495d421 commit 672aa21

2 files changed

Lines changed: 19 additions & 10 deletions

File tree

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DatetimeOutputCastRewriter.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,15 @@ final class DatetimeOutputCastRewriter {
8282
/**
8383
* PPL's documented timestamp output format (space separator). Mirrors the
8484
* format used by Calcite's reference planner so the analytics-engine path
85-
* matches per-row output exactly.
85+
* matches per-row output exactly. The trailing {@code %.f} is chrono's
86+
* variable-length fractional-second specifier — a leading dot followed by
87+
* 0-9 digits, omitted when the value has no sub-second precision. This
88+
* matches PPL's legacy formatting for {@code date} and {@code date_nanos}
89+
* fields where the displayed precision tracks the source value (e.g.
90+
* {@code "2024-01-15 10:30:01.23456789"} for a date_nanos with 8 fractional
91+
* digits, {@code "2025-08-01 03:47:41"} for a whole-second value).
8692
*/
87-
static final String PPL_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S";
93+
static final String PPL_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S%.f";
8894

8995
private DatetimeOutputCastRewriter() {}
9096

sandbox/plugins/analytics-backend-datafusion/src/test/java/org/opensearch/be/datafusion/DatetimeOutputCastRewriterTests.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@ public void setUp() throws Exception {
5555

5656
/**
5757
* Motivating shape: outer Project slot is exactly {@code CAST(<TIMESTAMP> AS VARCHAR)}.
58-
* Rewriter must replace it with a {@code TO_CHAR(<TIMESTAMP>, '%Y-%m-%d %H:%M:%S')}
59-
* call whose result type matches the original cast's VARCHAR type.
58+
* Rewriter must replace it with a {@code TO_CHAR(<TIMESTAMP>, '%Y-%m-%d %H:%M:%S%.f')}
59+
* call whose result type matches the original cast's VARCHAR type. The {@code %.f}
60+
* tail is chrono's variable-length fractional-seconds specifier so source values
61+
* with sub-second precision (DATE_NANOS) keep their fractional digits while
62+
* whole-second values display cleanly without a trailing decimal.
6063
*/
6164
public void testDirectTimestampOutputCastIsRewrittenToToChar() {
6265
RelDataType timestampType = typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.TIMESTAMP, 6), true);
@@ -236,11 +239,11 @@ public void testTimestampCastToCharIsUntouched() {
236239
}
237240

238241
/**
239-
* The format string is {@code "%Y-%m-%d %H:%M:%S"} — seconds-only — to match
240-
* Calcite's reference output for {@code CAST(TIMESTAMP AS VARCHAR)}, which
241-
* truncates fractional seconds. This pins that contract: a TIMESTAMP(9)
242-
* source still resolves to the seconds-only format literal in the
243-
* rewritten {@code TO_CHAR} call.
242+
* The format string is precision-agnostic: a TIMESTAMP(0) and a TIMESTAMP(9)
243+
* source both resolve to the same {@code "%Y-%m-%d %H:%M:%S%.f"} literal in the
244+
* rewritten {@code TO_CHAR} call. The {@code %.f} tail handles the runtime
245+
* variation — whole-second values render without a trailing decimal, sub-second
246+
* values render with as many fractional digits as the source carries.
244247
*/
245248
public void testTimestampPrecisionDoesNotChangeFormat() {
246249
RelDataType nanoTimestampType = typeFactory.createTypeWithNullability(typeFactory.createSqlType(SqlTypeName.TIMESTAMP, 9), true);
@@ -255,7 +258,7 @@ public void testTimestampPrecisionDoesNotChangeFormat() {
255258
RexCall call = (RexCall) ((LogicalProject) rewritten).getProjects().get(0);
256259
RexLiteral formatLit = (RexLiteral) call.getOperands().get(1);
257260
assertEquals(
258-
"TIMESTAMP(9) source must still resolve to the seconds-only format — fractional seconds are dropped",
261+
"Format literal is precision-agnostic — chrono's %.f handles the per-value fractional digits",
259262
DatetimeOutputCastRewriter.PPL_TIMESTAMP_FORMAT,
260263
formatLit.getValueAs(String.class)
261264
);

0 commit comments

Comments
 (0)