[analytics-engine] Rewrite engine-output CAST(TIMESTAMP AS VARCHAR) to to_char - #21650
Conversation
…o to_char
DatetimeOutputCastRule wraps datetime fields at the outermost LogicalProject
in CAST(... AS VARCHAR) so the unified planner stays backend-agnostic. Calcite
emits PPL's documented "2024-01-15 12:00:00" (space). DataFusion's Arrow CAST
kernel emits ISO-8601 "2024-01-15T12:00:00" (T-separator), and the
datafusion.format.timestamp_format session config affects only the CLI display
pipeline, not the cast kernel.
Add DatetimeOutputCastRewriter, a pre-isthmus pass that rewrites direct
project slots of shape CAST(<TIMESTAMP> AS VARCHAR) into
TO_CHAR(<TIMESTAMP>, '%Y-%m-%d %H:%M:%S'). Scope is intentionally narrow:
- Only direct project slots — nested casts inside CASE/COALESCE/UDF args
were authored by the user and must round-trip verbatim.
- Only standard SqlTypeName.TIMESTAMP sources — DATE and TIME cast cleanly
through Arrow already; TIMESTAMP_WITH_LOCAL_TIME_ZONE depends on the
DataFusion session timezone and is deferred until a concrete failing
case lands.
Wire SqlLibraryOperators.TO_CHAR through DataFusionFragmentConvertor's
ADDITIONAL_SCALAR_SIGS and declare a matching to_char extension in
opensearch_scalar_functions.yaml so DataFusion resolves the call to its
native to_char scalar.
Issue: opensearch-project/sql#5420.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
6f8f37b to
34c621c
Compare
PR Reviewer Guide 🔍(Review updated until commit e2f7b3a)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to e2f7b3a Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit d0057a0
Suggestions up to commit 657df0c
|
|
❌ Gradle check result for 657df0c: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
The unified planner wraps the output projection in a single LogicalSort (LogicalSystemLimit query-size cap), so the rewriter must descend through that Sort to reach the Project introduced by DatetimeOutputCastRule. Without this, the rewriter no-ops in production and the T-separator output returns despite the rule's output cast being present. Also broaden the Project match from LogicalProject to any Project subclass so OpenSearchProject (after OpenSearchProjectRule converts the rule's output) is still rewritten. Project#copy preserves the subclass. Add UTs: - testSortOverProjectDirectTimestampOutputCastIsRewritten - testSortOverNonProjectIsUntouched Drop a redundant Project cast on Project#copy. Issue: opensearch-project/sql#5420. Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
|
Persistent review updated to latest commit d0057a0 |
…his PR) Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
|
Persistent review updated to latest commit e2f7b3a |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21650 +/- ##
============================================
- Coverage 73.48% 73.45% -0.03%
+ Complexity 74736 74713 -23
============================================
Files 5983 5983
Lines 339062 339062
Branches 48882 48882
============================================
- Hits 249162 249061 -101
- Misses 70064 70165 +101
Partials 19836 19836 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…eOutputCastRewriter 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 (opensearch-project#21650)
closed with a seconds-only format that dropped the fractional digits
the tests still expect.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
…eOutputCastRewriter 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 (opensearch-project#21650)
closed with a seconds-only format that dropped the fractional digits
the tests still expect.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
…eOutputCastRewriter 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 (opensearch-project#21650)
closed with a seconds-only format that dropped the fractional digits
the tests still expect.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
…o to_char (opensearch-project#21650) * [analytics-engine] Rewrite engine-output CAST(TIMESTAMP AS VARCHAR) to to_char DatetimeOutputCastRule wraps datetime fields at the outermost LogicalProject in CAST(... AS VARCHAR) so the unified planner stays backend-agnostic. Calcite emits PPL's documented "2024-01-15 12:00:00" (space). DataFusion's Arrow CAST kernel emits ISO-8601 "2024-01-15T12:00:00" (T-separator), and the datafusion.format.timestamp_format session config affects only the CLI display pipeline, not the cast kernel. Add DatetimeOutputCastRewriter, a pre-isthmus pass that rewrites direct project slots of shape CAST(<TIMESTAMP> AS VARCHAR) into TO_CHAR(<TIMESTAMP>, '%Y-%m-%d %H:%M:%S'). Scope is intentionally narrow: - Only direct project slots — nested casts inside CASE/COALESCE/UDF args were authored by the user and must round-trip verbatim. - Only standard SqlTypeName.TIMESTAMP sources — DATE and TIME cast cleanly through Arrow already; TIMESTAMP_WITH_LOCAL_TIME_ZONE depends on the DataFusion session timezone and is deferred until a concrete failing case lands. Wire SqlLibraryOperators.TO_CHAR through DataFusionFragmentConvertor's ADDITIONAL_SCALAR_SIGS and declare a matching to_char extension in opensearch_scalar_functions.yaml so DataFusion resolves the call to its native to_char scalar. Issue: opensearch-project/sql#5420. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Handle Sort(Project) wrapper and any Project subclass The unified planner wraps the output projection in a single LogicalSort (LogicalSystemLimit query-size cap), so the rewriter must descend through that Sort to reach the Project introduced by DatetimeOutputCastRule. Without this, the rewriter no-ops in production and the T-separator output returns despite the rule's output cast being present. Also broaden the Project match from LogicalProject to any Project subclass so OpenSearchProject (after OpenSearchProjectRule converts the rule's output) is still rewritten. Project#copy preserves the subclass. Add UTs: - testSortOverProjectDirectTimestampOutputCastIsRewritten - testSortOverNonProjectIsUntouched Drop a redundant Project cast on Project#copy. Issue: opensearch-project/sql#5420. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * ci: retrigger sandbox-check (flaky composite-engine IT unrelated to this PR) Signed-off-by: Eric Wei <mengwei.eric@gmail.com> --------- Signed-off-by: Eric Wei <mengwei.eric@gmail.com> Signed-off-by: Khishorekumar BS <bkhishor@amazon.com>
…eOutputCastRewriter 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 (opensearch-project#21650)
closed with a seconds-only format that dropped the fractional digits
the tests still expect.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
…eOutputCastRewriter 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 (opensearch-project#21650)
closed with a seconds-only format that dropped the fractional digits
the tests still expect.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
…eOutputCastRewriter 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 (opensearch-project#21650)
closed with a seconds-only format that dropped the fractional digits
the tests still expect.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
…-engine route (#21681) * [analytics-backend-datafusion] Register TIMESTAMP in STANDARD_PROJECT_OPS PPL `timestamp(expr)` lowers to a `ScalarFunction.TIMESTAMP` call. The TimestampFunctionAdapter already wires the call into DataFusion's native `to_timestamp`, but `STANDARD_PROJECT_OPS` did not declare TIMESTAMP, so `OpenSearchProjectRule.annotateExpr` rejected every plan that contained the operator with "No backend supports scalar function [TIMESTAMP] among [datafusion]". Same call also shows up implicitly after the analyzer coerces a string literal to TIMESTAMP for column comparisons such as `@timestamp="2024-01-15T10:30:00Z"` once `@timestamp` is typed as TIMESTAMP (see the date_nanos schema fix in the previous commit). Update the inline comment block to match: the legacy-engine-only path the prior comment described is gone now that the capability is wired. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [analytics-engine] Catch Calcite Litmus.THROW AssertionError in DefaultPlanExecutor RexUtil.isFlat / RelOptUtil.eq / Project.isValid / RexChecker call into Calcite's Litmus.THROW, which raises AssertionError from raw Java code rather than via the `assert` keyword. JVM `-da` doesn't gate that path, so an assertion firing inside a search thread escapes to OpenSearchUncaughtExceptionHandler and exits the cluster JVM. This bit hard once `CalciteRelNodeVisitor` started lowering structured PPL `search` predicates to native filter shape: queries like `severityNumber="not-a-number"` fold to `=(SAFE_CAST($X), null)` ahead of the marking phase, and the Litmus check fires before any plan-executor listener gets to translate the error. The cluster died mid-IT and 21 subsequent tests failed with `Connection refused`. Convert AssertionError caught at the executor entrypoint to an IllegalStateException so the query reports as HTTP 500 with a bucketable message and the cluster survives. The same pattern is already in place at `UnifiedQueryPlanner.plan` on the SQL plugin side; this is the analytics-engine-side mirror so neither layer can produce a cluster-fatal assertion. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [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> * [analytics-backend-datafusion] Apply schema coercion on indexed-executor placeholder The indexed-execution path inferred the parquet schema via build_segments (which calls FileFormat::infer_schema) and registered it directly on the PlaceholderProvider that from_substrait_plan binds against. The non-indexed paths (session_context.rs, query_executor.rs, api.rs) all routed their inferred schemas through schema_coerce::coerce_inferred_schema before registering, but the indexed path skipped this step. Result: an OpenSearch `ip` column lands as parquet `BinaryView` on disk; isthmus on the Java side serializes the Substrait base schema as plain `Binary` (Substrait has no view types). The placeholder reports `BinaryView` while the plan declares `Binary` — DataFusion's substrait consumer rejects the bind with: Substrait error: Field '<x>' in Substrait schema has a different type (Binary) than the corresponding field in the table schema (BinaryView). Every analytics-engine query against an index that includes an `ip` column (every OTEL-logs query in CalciteSearchCommandIT, for example) fails at fragment start. Apply coerce_inferred_schema right after build_segments, before PlaceholderProvider construction. The placeholder, the expr_to_bool_tree analysis, and the downstream IndexedTableProvider all see the same Substrait-compatible (Binary / Int64 / Float32) schema, so the bind succeeds and the parquet reader's SchemaAdapter handles the per-batch BinaryView→Binary relabeling at scan time. This restores parity with the other infer_schema sites; no behavior change for non-IP columns. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
…-engine route (opensearch-project#21681) * [analytics-backend-datafusion] Register TIMESTAMP in STANDARD_PROJECT_OPS PPL `timestamp(expr)` lowers to a `ScalarFunction.TIMESTAMP` call. The TimestampFunctionAdapter already wires the call into DataFusion's native `to_timestamp`, but `STANDARD_PROJECT_OPS` did not declare TIMESTAMP, so `OpenSearchProjectRule.annotateExpr` rejected every plan that contained the operator with "No backend supports scalar function [TIMESTAMP] among [datafusion]". Same call also shows up implicitly after the analyzer coerces a string literal to TIMESTAMP for column comparisons such as `@timestamp="2024-01-15T10:30:00Z"` once `@timestamp` is typed as TIMESTAMP (see the date_nanos schema fix in the previous commit). Update the inline comment block to match: the legacy-engine-only path the prior comment described is gone now that the capability is wired. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [analytics-engine] Catch Calcite Litmus.THROW AssertionError in DefaultPlanExecutor RexUtil.isFlat / RelOptUtil.eq / Project.isValid / RexChecker call into Calcite's Litmus.THROW, which raises AssertionError from raw Java code rather than via the `assert` keyword. JVM `-da` doesn't gate that path, so an assertion firing inside a search thread escapes to OpenSearchUncaughtExceptionHandler and exits the cluster JVM. This bit hard once `CalciteRelNodeVisitor` started lowering structured PPL `search` predicates to native filter shape: queries like `severityNumber="not-a-number"` fold to `=(SAFE_CAST($X), null)` ahead of the marking phase, and the Litmus check fires before any plan-executor listener gets to translate the error. The cluster died mid-IT and 21 subsequent tests failed with `Connection refused`. Convert AssertionError caught at the executor entrypoint to an IllegalStateException so the query reports as HTTP 500 with a bucketable message and the cluster survives. The same pattern is already in place at `UnifiedQueryPlanner.plan` on the SQL plugin side; this is the analytics-engine-side mirror so neither layer can produce a cluster-fatal assertion. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [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 (opensearch-project#21650) closed with a seconds-only format that dropped the fractional digits the tests still expect. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [analytics-backend-datafusion] Apply schema coercion on indexed-executor placeholder The indexed-execution path inferred the parquet schema via build_segments (which calls FileFormat::infer_schema) and registered it directly on the PlaceholderProvider that from_substrait_plan binds against. The non-indexed paths (session_context.rs, query_executor.rs, api.rs) all routed their inferred schemas through schema_coerce::coerce_inferred_schema before registering, but the indexed path skipped this step. Result: an OpenSearch `ip` column lands as parquet `BinaryView` on disk; isthmus on the Java side serializes the Substrait base schema as plain `Binary` (Substrait has no view types). The placeholder reports `BinaryView` while the plan declares `Binary` — DataFusion's substrait consumer rejects the bind with: Substrait error: Field '<x>' in Substrait schema has a different type (Binary) than the corresponding field in the table schema (BinaryView). Every analytics-engine query against an index that includes an `ip` column (every OTEL-logs query in CalciteSearchCommandIT, for example) fails at fragment start. Apply coerce_inferred_schema right after build_segments, before PlaceholderProvider construction. The placeholder, the expr_to_bool_tree analysis, and the downstream IndexedTableProvider all see the same Substrait-compatible (Binary / Int64 / Float32) schema, so the bind succeeds and the parquet reader's SchemaAdapter handles the per-batch BinaryView→Binary relabeling at scan time. This restores parity with the other infer_schema sites; no behavior change for non-IP columns. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
…-engine route (opensearch-project#21681) * [analytics-backend-datafusion] Register TIMESTAMP in STANDARD_PROJECT_OPS PPL `timestamp(expr)` lowers to a `ScalarFunction.TIMESTAMP` call. The TimestampFunctionAdapter already wires the call into DataFusion's native `to_timestamp`, but `STANDARD_PROJECT_OPS` did not declare TIMESTAMP, so `OpenSearchProjectRule.annotateExpr` rejected every plan that contained the operator with "No backend supports scalar function [TIMESTAMP] among [datafusion]". Same call also shows up implicitly after the analyzer coerces a string literal to TIMESTAMP for column comparisons such as `@timestamp="2024-01-15T10:30:00Z"` once `@timestamp` is typed as TIMESTAMP (see the date_nanos schema fix in the previous commit). Update the inline comment block to match: the legacy-engine-only path the prior comment described is gone now that the capability is wired. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [analytics-engine] Catch Calcite Litmus.THROW AssertionError in DefaultPlanExecutor RexUtil.isFlat / RelOptUtil.eq / Project.isValid / RexChecker call into Calcite's Litmus.THROW, which raises AssertionError from raw Java code rather than via the `assert` keyword. JVM `-da` doesn't gate that path, so an assertion firing inside a search thread escapes to OpenSearchUncaughtExceptionHandler and exits the cluster JVM. This bit hard once `CalciteRelNodeVisitor` started lowering structured PPL `search` predicates to native filter shape: queries like `severityNumber="not-a-number"` fold to `=(SAFE_CAST($X), null)` ahead of the marking phase, and the Litmus check fires before any plan-executor listener gets to translate the error. The cluster died mid-IT and 21 subsequent tests failed with `Connection refused`. Convert AssertionError caught at the executor entrypoint to an IllegalStateException so the query reports as HTTP 500 with a bucketable message and the cluster survives. The same pattern is already in place at `UnifiedQueryPlanner.plan` on the SQL plugin side; this is the analytics-engine-side mirror so neither layer can produce a cluster-fatal assertion. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [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 (opensearch-project#21650) closed with a seconds-only format that dropped the fractional digits the tests still expect. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [analytics-backend-datafusion] Apply schema coercion on indexed-executor placeholder The indexed-execution path inferred the parquet schema via build_segments (which calls FileFormat::infer_schema) and registered it directly on the PlaceholderProvider that from_substrait_plan binds against. The non-indexed paths (session_context.rs, query_executor.rs, api.rs) all routed their inferred schemas through schema_coerce::coerce_inferred_schema before registering, but the indexed path skipped this step. Result: an OpenSearch `ip` column lands as parquet `BinaryView` on disk; isthmus on the Java side serializes the Substrait base schema as plain `Binary` (Substrait has no view types). The placeholder reports `BinaryView` while the plan declares `Binary` — DataFusion's substrait consumer rejects the bind with: Substrait error: Field '<x>' in Substrait schema has a different type (Binary) than the corresponding field in the table schema (BinaryView). Every analytics-engine query against an index that includes an `ip` column (every OTEL-logs query in CalciteSearchCommandIT, for example) fails at fragment start. Apply coerce_inferred_schema right after build_segments, before PlaceholderProvider construction. The placeholder, the expr_to_bool_tree analysis, and the downstream IndexedTableProvider all see the same Substrait-compatible (Binary / Int64 / Float32) schema, so the bind succeeds and the parquet reader's SchemaAdapter handles the per-batch BinaryView→Binary relabeling at scan time. This restores parity with the other infer_schema sites; no behavior change for non-IP columns. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
Fixes opensearch-project/sql#5420.
Problem
PPL's documented timestamp wire format uses a space separator (
"2024-01-15 12:00:00"). On the analytics-engine route,DatetimeOutputCastRulewraps every output datetime field inCAST(<datetime> AS VARCHAR)so the unified planner stays backend-agnostic. Calcite renders that cast as the documented space-separated form; DataFusion's Arrow CAST kernel renders it ISO-8601 ("2024-01-15T12:00:00"). Thedatafusion.format.timestamp_formatsession config only affects DataFusion's CLI display pipeline, not the cast kernel.Approach
A small pre-Substrait rewrite at the analytics-engine boundary swaps the engine-output cast for an explicit format call:
Routed to DataFusion's native
to_charscalar via the existing Substrait simple-extension mechanism — no new UDFs.Scope is intentionally narrow:
Projectsubclass (LogicalProjectorOpenSearchProject) and descends through the unified planner's singleLogicalSortsystem-limit wrapper when present. Inner projections round-trip verbatim. Not recursive.CASE/COALESCE/UDF arguments round-trip verbatim.TIMESTAMP → VARCHARonly.DATE/TIMEcast cleanly through Arrow already;CHAR(n)andTIMESTAMP_WITH_LOCAL_TIME_ZONEare out of scope.DatetimeOutputCastRule's introducing PR (sql#5408): "PPL Calcite produces ANSI SQL format2024-01-15 12:00:00."TIMESTAMP(9)sources resolve to the same seconds-only output.Performance
Negligible. Plan-time: a single pass over the root projection's slot list, bounded by SELECT-list width. Runtime: replaces one row-level formatter with another in the same projection — no extra physical stage, same node count, on timestamp output columns only.
Test plan
DatetimeOutputCastRewriterTests— direct cast rewrite,Sort(Project)wrapper handling, inner projection untouched, nested cast insideCASEuntouched,CHAR(n)untouched,DATE/TIME/integer sources untouched, plain field reference untouched,TIMESTAMP(9)source resolves to seconds-only format.DataFusionFragmentConvertorTests#testProjectTimestampOutputCastEmitsToCharExtension— end-to-end Substrait serialization asserts the formatted-render extension function (not a raw cast) is emitted../gradlew check -p sandbox -Dsandbox.enabled=true.now()/current_timestamp()regression class failed withT-separator output; post-fix the same calls return space-separated strings and pass.