[Analytics Backend / DataFusion] Wire SPAN scalar through analytics-engine route#21584
Conversation
PR Reviewer Guide 🔍(Review updated until commit 92e6cb1)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 92e6cb1 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 7adbc56
Suggestions up to commit dc70c68
Suggestions up to commit 77d2cc1
Suggestions up to commit f10a09b
Suggestions up to commit a2a5a92
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21584 +/- ##
=========================================
Coverage 73.50% 73.51%
+ Complexity 74753 74711 -42
=========================================
Files 5984 5985 +1
Lines 339118 339137 +19
Branches 48895 48894 -1
=========================================
+ Hits 249273 249303 +30
+ Misses 69993 69973 -20
- Partials 19852 19861 +9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
❌ Gradle check result for f10a09b: null 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? |
f10a09b to
77d2cc1
Compare
|
Rebased onto post-#21457 main. Adjusted to the new framework:
Local compile + spotless clean on the rebased branch. Unit tests pending in CI. |
|
Persistent review updated to latest commit 77d2cc1 |
|
❌ Gradle check result for 77d2cc1: 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? |
|
Persistent review updated to latest commit dc70c68 |
|
❌ Gradle check result for dc70c68: 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? |
dc70c68 to
71ea628
Compare
Three percentile-related tests fail on the force-routed analytics-engine path for reasons that are not test-side fixable in this PR's scope: - testStatsPercentileWithNull: DataFusion's TDigest implementation interpolates the 50th percentile to 35413 where OpenSearch's TDigest interpolates to 39225 on the same input. Both values are valid approximations within TDigest's compression-bound error. Record the per-engine value via isAnalyticsForceRoutingEnabled() so the test documents the divergence rather than masking it. - testStatsPercentileBySpan: same TDigest divergence shows up on the age=30 bucket (33194 on analytics vs 39225 on legacy). The age=20 bucket happens to coincide. Same per-engine encoding. - testStatsBySpanTimeWithNullBucket: span(@timestamp, 12h) uses a multi-unit time interval that the current SpanAdapter doesn't rewrite (only interval=1 → DATE_TRUNC). Skip via Assume.assumeFalse on analytics until multi-unit time-span support lands (likely via DataFusion's date_bin). These are coordinated with opensearch-project/OpenSearch#21584 commit 5 which fixes the upstream planner-side type mismatch for percentile + group-by, so the queries now reach the TDigest-divergence assertion rather than crashing with a 500. Signed-off-by: Kai Huang <ahkcs@amazon.com>
When -Dtests.analytics.parquet_indices=true is set, every test-created index is parquet-backed and RestUnifiedQueryAction.isAnalyticsIndex routes all queries through the analytics-engine backend (DataFusion). Eight tests need analytics-specific assertions because DataFusion follows different ordering, null-bucket, and SQL-spec semantics than the legacy V2 / Calcite-DSL paths: Null-bucket / SQL-spec semantics: - testStatsWithLimit Q1+Q2 — head 5 over a 6-bucket result picks (null,36) instead of (null,null); the same effect cascades into `head 2 from 1`. Use the size-only assertion branch already present for the no-pushdown case. - testDisableLegacyPreferred — under PPL_SYNTAX_LEGACY_PREFERRED=false, the V2 / Calcite-DSL path drops the null-age bucket (5 rows); the analytics-engine backend keeps it (6 rows). Provide an explicit 6-row expectation for the analytics branch. - testSumWithNull — DataFusion follows SQL spec like Calcite-no-pushdown — SUM of all-null is null, not 0. OR analytics into the existing isPushdownDisabled branch. - testStatsPercentileByNullValue + NonNullBucket — same SQL-spec behaviour for percentile — DataFusion returns null for empty / all-null groups where legacy DSL pushdown returns 0. TDigest interpolation + multi-unit time span: - testStatsPercentileWithNull — DataFusion TDigest interpolates p50 to 35413 vs OpenSearch's 39225. Both are valid approximations within TDigest's compression-bound error. Per-engine expected value. - testStatsPercentileBySpan — same TDigest diff on the age=30 bucket (33194 vs 39225). Coordinated with opensearch-project/OpenSearch#21584 commit 5 which fixes the upstream planner-side type mismatch so the query reaches the data assertion at all. - testStatsBySpanTimeWithNullBucket — span(@timestamp, 12h) multi-unit time interval unsupported in current SpanAdapter. Skipped on the analytics backend via Assume.assumeFalse until DataFusion's date_bin lands. Signed-off-by: Kai Huang <ahkcs@amazon.com>
When -Dtests.analytics.parquet_indices=true is set, every test-created index is parquet-backed and RestUnifiedQueryAction.isAnalyticsIndex routes all queries through the analytics-engine backend (DataFusion). Seven tests need analytics-specific assertions because DataFusion follows different null-bucket, SQL-spec, and TDigest interpolation semantics than the legacy V2 / Calcite-DSL paths: SQL-spec semantics (DataFusion follows the spec; legacy DSL returns 0): - testSumWithNull — SUM of all-null returns null on analytics; the existing isPushdownDisabled branch already handles the Calcite-no- pushdown case. OR analytics into that branch. - testStatsPercentileByNullValue + NonNullBucket — percentile of an all-null / empty group returns null on analytics. Same OR pattern. Non-deterministic head ordering: - testStatsWithLimit Q1+Q2 — head 5 over a 6-bucket result picks a different null-balance row than the legacy / Calcite-DSL path; same effect cascades into `head 2 from 1`. Reuse the size-only assertion branch already present for the no-pushdown case. TDigest interpolation divergence (genuine impl difference): - testStatsPercentileWithNull — DataFusion TDigest interpolates p50 to 35413 vs OpenSearch's 39225; both within compression-bound error. Per-engine expected value. - testStatsPercentileBySpan — same TDigest diff on the age=30 bucket (33194 vs 39225). Coordinated with opensearch-project/OpenSearch#21584 commit 5 which fixes the upstream planner-side type mismatch so the query reaches the data assertion at all. Semantic divergence pending team decision: - testDisableLegacyPreferred — under PPL_SYNTAX_LEGACY_PREFERRED=false, V2 / Calcite-DSL drop the null-age bucket (5 rows) while DataFusion keeps it (6 rows). Skipped on the analytics path via Assume.assumeFalse until the team decides which behaviour is the intended new-syntax semantics. Out of scope for this PR (intentionally left failing on the analytics path so the gap is visible in CI): - testStatsBySpanTimeWithNullBucket — span(@timestamp, 12h) multi-unit time interval unsupported in current SpanAdapter (only interval=1 is rewritten to DATE_TRUNC; multi-unit needs DataFusion's date_bin). Signed-off-by: Kai Huang <ahkcs@amazon.com>
sandeshkr419
left a comment
There was a problem hiding this comment.
Hi,
Functions: avg/stddev/related are already pushed in with my changes and handled by Calcite decomposition directly, I think you can repurpose this PR for span explicitly.
0625fdf to
7adbc56
Compare
|
Persistent review updated to latest commit 7adbc56 |
…verage (#5436) * Add tests.analytics.parquet_indices test toggle Adds a small, opt-in test infrastructure slice so the PPL integration test suite can run end-to-end against the analytics-engine backend without per-test rewiring. `-Dtests.analytics.parquet_indices=true` makes `TestUtils.createIndexByRestClient` back every test-created index with single-shard composite/parquet storage: index.pluggable.dataformat.enabled = true index.pluggable.dataformat = "composite" index.composite.primary_data_format = "parquet" `RestUnifiedQueryAction.isAnalyticsIndex` (post-#5432) reads these settings and routes any query against such indices to the analytics-engine planner (DataFusion). No additional cluster setting or routing override required — the production routing logic is the single source of truth. Also adds `PPLIntegTestCase.isAnalyticsParquetIndicesEnabled()` as a per-test predicate so individual tests can branch their assertions on engine semantics (DataFusion follows different ordering and null-bucket semantics than the legacy V2 and Calcite-DSL paths). Bulk loads on parquet-backed indices use `refresh=true` because `analytics-backend-lucene`'s `LuceneCommitter.getSafeCommitInfo` is a `TODO:: with index deleter` stub that hangs `refresh=wait_for` until the test framework request timeout (~60s). `integ-test/build.gradle` forwards the property to `:integTestRemote` so the gradle command line is the single knob. Default behavior is unchanged — with the flag unset, every test-created index is Lucene-backed and every IT runs through the existing V2 / Calcite path exactly as before. Signed-off-by: Kai Huang <ahkcs@amazon.com> * Branch 7 stats tests on isAnalyticsParquetIndicesEnabled When -Dtests.analytics.parquet_indices=true is set, every test-created index is parquet-backed and RestUnifiedQueryAction.isAnalyticsIndex routes all queries through the analytics-engine backend (DataFusion). Seven tests need analytics-specific assertions because DataFusion follows different null-bucket, SQL-spec, and TDigest interpolation semantics than the legacy V2 / Calcite-DSL paths: SQL-spec semantics (DataFusion follows the spec; legacy DSL returns 0): - testSumWithNull — SUM of all-null returns null on analytics; the existing isPushdownDisabled branch already handles the Calcite-no- pushdown case. OR analytics into that branch. - testStatsPercentileByNullValue + NonNullBucket — percentile of an all-null / empty group returns null on analytics. Same OR pattern. Non-deterministic head ordering: - testStatsWithLimit Q1+Q2 — head 5 over a 6-bucket result picks a different null-balance row than the legacy / Calcite-DSL path; same effect cascades into `head 2 from 1`. Reuse the size-only assertion branch already present for the no-pushdown case. TDigest interpolation divergence (genuine impl difference): - testStatsPercentileWithNull — DataFusion TDigest interpolates p50 to 35413 vs OpenSearch's 39225; both within compression-bound error. Per-engine expected value. - testStatsPercentileBySpan — same TDigest diff on the age=30 bucket (33194 vs 39225). Coordinated with opensearch-project/OpenSearch#21584 commit 5 which fixes the upstream planner-side type mismatch so the query reaches the data assertion at all. Semantic divergence pending team decision: - testDisableLegacyPreferred — under PPL_SYNTAX_LEGACY_PREFERRED=false, V2 / Calcite-DSL drop the null-age bucket (5 rows) while DataFusion keeps it (6 rows). Skipped on the analytics path via Assume.assumeFalse until the team decides which behaviour is the intended new-syntax semantics. Out of scope for this PR (intentionally left failing on the analytics path so the gap is visible in CI): - testStatsBySpanTimeWithNullBucket — span(@timestamp, 12h) multi-unit time interval unsupported in current SpanAdapter (only interval=1 is rewritten to DATE_TRUNC; multi-unit needs DataFusion's date_bin). Signed-off-by: Kai Huang <ahkcs@amazon.com> * Consolidate analytics parquet config into AnalyticsIndexConfig Per @dai-chen's review feedback — the index-creation settings, the bulk-load refresh strategy, and the system-property gate were spread across three places in TestUtils (the `ANALYTICS_PARQUET_INDICES_PROP` constant, the inline check in `createIndexByRestClient`, the inline check in `loadDataByRestClient`, and the private `makeParquetBacked` helper). That's the same spread-out-conditional pattern the OS-Spark repo's AOSS configs ended up with. Collect everything into a single `TestUtils.AnalyticsIndexConfig` nested helper: * `ENABLED_PROP` — the system property name. * `isEnabled()` — single source for the gate. * `applyIndexCreationSettings(jsonObject)` — injects the parquet-backed composite settings; no-op when disabled. * `bulkLoadRefreshParam()` — returns the right `_bulk` refresh query string for the active index type. `createIndexByRestClient` and `loadDataByRestClient` call these unconditionally — the methods themselves short-circuit when the config is off — so the parquet-specific branching is gone from the helpers and concentrated in one place. `PPLIntegTestCase.isAnalyticsParquetIndicesEnabled()` now delegates to `AnalyticsIndexConfig.isEnabled()`. The legacy `ANALYTICS_PARQUET_INDICES_PROP` constant is kept as a forwarding alias for any external callers. Verified end-to-end against the analytics-engine cluster — same 50 / 63 pass + 1 skip + 12 fail as the pre-refactor run, so the consolidation is behavior-preserving. Signed-off-by: Kai Huang <huangkaics@gmail.com> Signed-off-by: Kai Huang <ahkcs@amazon.com> * Revert per-engine assertions on percentile tests Per @dai-chen — since the companion OpenSearch PR (#21584) no longer wires `percentile_approx` on the analytics path (Sandesh's "repurpose for SPAN" scope), all `percentile(...)` queries fail at execution before reaching the assertion. The per-engine branches that this PR introduced for `testStatsPercentile{WithNull,BySpan,ByNullValue, ByNullValueNonNullBucket}` were doing nothing in practice — the tests fail regardless. Reverts those four tests to their pre-PR state. The three remaining intentional changes stand: * `testStatsWithLimit` Q1+Q2 — non-deterministic head ordering on DataFusion hash-bucket order; size-only branch on the analytics path. * `testSumWithNull` — extends the existing `isPushdownDisabled() ? null : 0` pattern. SUM works on the analytics path and DataFusion returns null per SQL spec. * `testDisableLegacyPreferred` — `Assume.assumeFalse` skip for the pending-team-decision null-bucket semantic divergence. Signed-off-by: Kai Huang <huangkaics@gmail.com> Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com> Signed-off-by: Kai Huang <huangkaics@gmail.com>
Adds the PPL `span(field, interval, unit?)` scalar — the bucketing
primitive used by `stats … by span(...)`. Lowers to substrait-default
primitives so DataFusion's substrait consumer accepts the plan
verbatim:
- Numeric span (`span(field, N)`) → FLOOR(field/interval)*interval
(or integer division for INT fields, where Calcite's truncating
semantics already produce the bucket floor). The numeric result is
CAST'd back to the SPAN call's declared return type to avoid the
surrounding Project's typeMatchesInferred check failing on
Calcite's wider DECIMAL precision inference for
multiply(field/interval, interval).
- Time span with interval=1 (`span(birthdate, 1y)`,
`span(ts, 1month)`) → DATE_TRUNC(<unit>, field). PPL's SpanUnit
single-letter codes map to DataFusion's
microsecond / millisecond / second / minute / hour / day / week /
month / quarter / year strings.
- Multi-unit time intervals like `12h` aren't expressible as
date_trunc and fall through unchanged, surfacing as a normal
substrait binding error rather than a silent wrong-result.
Multi-unit support can be added later via DataFusion's `date_bin`.
Five files:
- ScalarFunction.SPAN enum entry (analytics-framework SPI).
- SpanAdapter (new) — performs the rewrite.
- DataFusionAnalyticsBackendPlugin — registers SpanAdapter +
ScalarFunction.SPAN in PROJECT_SCALAR_FUNCTIONS.
- opensearch_scalar_functions.yaml — declares `date_trunc` so the
SpanAdapter's time-span rewrite resolves through isthmus binding.
- DataFusionFragmentConvertor — adds the SqlLibraryOperators.DATE_TRUNC
→ "date_trunc" mapping.
AVG / STDDEV_POP / STDDEV_SAMP / VAR_POP / VAR_SAMP are not touched —
they already reach DataFusion via Calcite's
OpenSearchAggregateReduceRule decomposition (introduced in opensearch-project#21645)
which rewrites them to SUM/COUNT/DIVIDE/POWER/CAST primitives before
the substrait converter runs.
Signed-off-by: Kai Huang <huangkaics@gmail.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Self-contained integration test under sandbox/qa/analytics-engine-rest mirroring CalciteStatsCommandIT from the opensearch-project/sql repository. Each test sends a PPL query through POST /_analytics/ppl (exposed by test-ppl-frontend), exercising the same UnifiedQueryPlanner → CalciteRelNodeVisitor → Substrait → DataFusion pipeline as the SQL-side ITs, but inside core without depending on the SQL plugin. 16 tests over the parquet-backed `calcs` dataset cover the operators reachable from this PR — SPAN over numerics (the wiring added here) plus the broader stats family that already flows through the analytics route via OpenSearchAggregateReduceRule (opensearch-project#21645) decomposition: * single-value aggregates: count, count(field), sum, avg, min/max, distinct_count * filter + aggregate * group-by aggregates (with explicit `| sort` to pin order, since hash-aggregation output is non-deterministic) * statistical aggregates: stddev_pop / stddev_samp, var_pop / var_samp * numeric span: count() by span(int0, 5) Group-by tests use explicit `| sort` because the analytics-engine backend's hash-aggregation output order is non-deterministic. Signed-off-by: Kai Huang <huangkaics@gmail.com> Signed-off-by: Kai Huang <ahkcs@amazon.com>
7adbc56 to
92e6cb1
Compare
|
Persistent review updated to latest commit 92e6cb1 |
…ngine route (opensearch-project#21584) * [Analytics Backend / DataFusion] Wire SPAN scalar for stats Adds the PPL `span(field, interval, unit?)` scalar — the bucketing primitive used by `stats … by span(...)`. Lowers to substrait-default primitives so DataFusion's substrait consumer accepts the plan verbatim: - Numeric span (`span(field, N)`) → FLOOR(field/interval)*interval (or integer division for INT fields, where Calcite's truncating semantics already produce the bucket floor). The numeric result is CAST'd back to the SPAN call's declared return type to avoid the surrounding Project's typeMatchesInferred check failing on Calcite's wider DECIMAL precision inference for multiply(field/interval, interval). - Time span with interval=1 (`span(birthdate, 1y)`, `span(ts, 1month)`) → DATE_TRUNC(<unit>, field). PPL's SpanUnit single-letter codes map to DataFusion's microsecond / millisecond / second / minute / hour / day / week / month / quarter / year strings. - Multi-unit time intervals like `12h` aren't expressible as date_trunc and fall through unchanged, surfacing as a normal substrait binding error rather than a silent wrong-result. Multi-unit support can be added later via DataFusion's `date_bin`. Five files: - ScalarFunction.SPAN enum entry (analytics-framework SPI). - SpanAdapter (new) — performs the rewrite. - DataFusionAnalyticsBackendPlugin — registers SpanAdapter + ScalarFunction.SPAN in PROJECT_SCALAR_FUNCTIONS. - opensearch_scalar_functions.yaml — declares `date_trunc` so the SpanAdapter's time-span rewrite resolves through isthmus binding. - DataFusionFragmentConvertor — adds the SqlLibraryOperators.DATE_TRUNC → "date_trunc" mapping. AVG / STDDEV_POP / STDDEV_SAMP / VAR_POP / VAR_SAMP are not touched — they already reach DataFusion via Calcite's OpenSearchAggregateReduceRule decomposition (introduced in opensearch-project#21645) which rewrites them to SUM/COUNT/DIVIDE/POWER/CAST primitives before the substrait converter runs. Signed-off-by: Kai Huang <huangkaics@gmail.com> Signed-off-by: Kai Huang <ahkcs@amazon.com> * [QA] Add self-contained StatsCommandIT mirror Self-contained integration test under sandbox/qa/analytics-engine-rest mirroring CalciteStatsCommandIT from the opensearch-project/sql repository. Each test sends a PPL query through POST /_analytics/ppl (exposed by test-ppl-frontend), exercising the same UnifiedQueryPlanner → CalciteRelNodeVisitor → Substrait → DataFusion pipeline as the SQL-side ITs, but inside core without depending on the SQL plugin. 16 tests over the parquet-backed `calcs` dataset cover the operators reachable from this PR — SPAN over numerics (the wiring added here) plus the broader stats family that already flows through the analytics route via OpenSearchAggregateReduceRule (opensearch-project#21645) decomposition: * single-value aggregates: count, count(field), sum, avg, min/max, distinct_count * filter + aggregate * group-by aggregates (with explicit `| sort` to pin order, since hash-aggregation output is non-deterministic) * statistical aggregates: stddev_pop / stddev_samp, var_pop / var_samp * numeric span: count() by span(int0, 5) Group-by tests use explicit `| sort` because the analytics-engine backend's hash-aggregation output order is non-deterministic. Signed-off-by: Kai Huang <huangkaics@gmail.com> Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <huangkaics@gmail.com> Signed-off-by: Kai Huang <ahkcs@amazon.com> Signed-off-by: Khishorekumar BS <bkhishor@amazon.com>
…er_*
Adds a peephole adapter that constant-folds the exact expression shape PPL
timechart's per_second / per_minute / per_hour / per_day aggregations
produce:
TIMESTAMPDIFF(out_unit, t, TIMESTAMPADD(in_unit, n, t))
When both units are fixed-length (MICROSECOND through WEEK) and the inner
TIMESTAMPADD's base is the same RexNode as the outer TIMESTAMPDIFF's start,
the expression evaluates to the constant n * (in_ms / out_ms) regardless of
t. The adapter materializes that BIGINT literal and lets it flow through
Substrait — eliminating both PPL UDF references in one step (neither has a
Substrait extension binding).
OperatorAnnotation wrappers (AnnotatedProjectExpression introduced by
OpenSearchProjectRule) are peeled at each operand before the structural
comparison, so the wrapped inner TIMESTAMPADD remains recognizable instead
of looking like an annotation RexCall whose operator is ANNOTATED_PROJECT_EXPR.
Out of scope (call falls through unchanged):
- Variable-length out_unit / in_unit (MONTH / QUARTER / YEAR) — fold isn't
safe because milliseconds-per-month depends on which month t lands in.
testTimechartPerSecondWithVariableMonthLengths is an example: span=1M
with per_second expects different per-row results for Feb vs Oct.
- Standalone TIMESTAMPADD (not nested inside TIMESTAMPDIFF) — no adapter
registered for TIMESTAMPADD on its own.
- Non-literal unit / n operands — the peephole requires both to be string
/ integer literals.
Pass-rate impact on the subset this adapter targets (CalciteTimechartPerFunctionIT):
- Before: 6 tests blocked at "Unable to convert call TIMESTAMPADD(string, i32, ...)".
- After: 1 test now reaches client-side assertion (sql#5420 schema-string
cast), 4 tests reveal the orthogonal multi-unit SPAN limitation
(opensearch-project#21584's known gap), 1 test correctly stays blocked on
variable-month-length.
CalciteStatsCommandIT remains at 47/63 (no regression).
Signed-off-by: Kai Huang <huangkaics@gmail.com>
Resolves the 13 by-clause failures in CalciteTimechartCommandIT (5/18 → 18/18) caused by DataFusion's row_number() physical op emitting UInt64 while Calcite types ROW_NUMBER OVER as BIGINT (Int64 signed). Two coupled changes mirror the existing schema_coerce::coerce_inferred_schema bridge the parquet-scan path uses, applied at the partition-stream boundary where the analytics-engine reduce stage reads from upstream PARTIAL fragments via NativeBridge.registerPartitionStream. 1. api::register_partition_stream — coerce the producer's physical schema via schema_coerce before registering the StreamingTable and before writing the IPC bytes back to Java. The Substrait consumer plan's `ensure_schema_compatibility` (datafusion-substrait 53) strict-checks each ReadRel's base_schema against the registered table; the coercion makes Int64-declared plans bind against this table. The Java-side typesMatch tripwire then sees the coerced schema and validates incoming batches against it (next change). 2. api::sender_send — coerce incoming RecordBatches to match the sender's (now-coerced) schema via arrow::compute::cast where types differ. Without this, DataFusion's HashJoin / RepartitionExec panics with "primitive array" on as_primitive::<Int64Type>() of a UInt64 column when the producer's actual batch reaches downstream operators. cast() is the existing Arrow kernel — no-op when the schemas already match, single-cast on the mismatched columns otherwise. Same width preserves zero-copy semantics modulo the bit-reinterpret arrow handles internally. 3. DatafusionReduceSink.typesMatch — relax Java's tripwire to treat same-width Int (any signedness) as compatible, mirroring the existing Timestamp(any precision) tolerance. Same rationale: divergence is harmless because the sender_send coercion produces a downstream- compatible batch. Pass-rate impact: - CalciteTimechartCommandIT: 5/18 → 18/18 ✅ (+13) - CalciteTimechartPerFunctionIT: 1/12 → 1/12 (residuals are opensearch-project#21584's multi-unit-SPAN gap + variable-month TIMESTAMPADD) - Combined timechart: 6/30 → 19/30 (63%) Signed-off-by: Kai Huang <huangkaics@gmail.com>
…er_*
Adds a peephole adapter that constant-folds the exact expression shape PPL
timechart's per_second / per_minute / per_hour / per_day aggregations
produce:
TIMESTAMPDIFF(out_unit, t, TIMESTAMPADD(in_unit, n, t))
When both units are fixed-length (MICROSECOND through WEEK) and the inner
TIMESTAMPADD's base is the same RexNode as the outer TIMESTAMPDIFF's start,
the expression evaluates to the constant n * (in_ms / out_ms) regardless of
t. The adapter materializes that BIGINT literal and lets it flow through
Substrait — eliminating both PPL UDF references in one step (neither has a
Substrait extension binding).
OperatorAnnotation wrappers (AnnotatedProjectExpression introduced by
OpenSearchProjectRule) are peeled at each operand before the structural
comparison, so the wrapped inner TIMESTAMPADD remains recognizable instead
of looking like an annotation RexCall whose operator is ANNOTATED_PROJECT_EXPR.
Out of scope (call falls through unchanged):
- Variable-length out_unit / in_unit (MONTH / QUARTER / YEAR) — fold isn't
safe because milliseconds-per-month depends on which month t lands in.
testTimechartPerSecondWithVariableMonthLengths is an example: span=1M
with per_second expects different per-row results for Feb vs Oct.
- Standalone TIMESTAMPADD (not nested inside TIMESTAMPDIFF) — no adapter
registered for TIMESTAMPADD on its own.
- Non-literal unit / n operands — the peephole requires both to be string
/ integer literals.
Pass-rate impact on the subset this adapter targets (CalciteTimechartPerFunctionIT):
- Before: 6 tests blocked at "Unable to convert call TIMESTAMPADD(string, i32, ...)".
- After: 1 test now reaches client-side assertion (sql#5420 schema-string
cast), 4 tests reveal the orthogonal multi-unit SPAN limitation
(opensearch-project#21584's known gap), 1 test correctly stays blocked on
variable-month-length.
CalciteStatsCommandIT remains at 47/63 (no regression).
Signed-off-by: Kai Huang <huangkaics@gmail.com>
Resolves the 13 by-clause failures in CalciteTimechartCommandIT (5/18 → 18/18) caused by DataFusion's row_number() physical op emitting UInt64 while Calcite types ROW_NUMBER OVER as BIGINT (Int64 signed). Two coupled changes mirror the existing schema_coerce::coerce_inferred_schema bridge the parquet-scan path uses, applied at the partition-stream boundary where the analytics-engine reduce stage reads from upstream PARTIAL fragments via NativeBridge.registerPartitionStream. 1. api::register_partition_stream — coerce the producer's physical schema via schema_coerce before registering the StreamingTable and before writing the IPC bytes back to Java. The Substrait consumer plan's `ensure_schema_compatibility` (datafusion-substrait 53) strict-checks each ReadRel's base_schema against the registered table; the coercion makes Int64-declared plans bind against this table. The Java-side typesMatch tripwire then sees the coerced schema and validates incoming batches against it (next change). 2. api::sender_send — coerce incoming RecordBatches to match the sender's (now-coerced) schema via arrow::compute::cast where types differ. Without this, DataFusion's HashJoin / RepartitionExec panics with "primitive array" on as_primitive::<Int64Type>() of a UInt64 column when the producer's actual batch reaches downstream operators. cast() is the existing Arrow kernel — no-op when the schemas already match, single-cast on the mismatched columns otherwise. Same width preserves zero-copy semantics modulo the bit-reinterpret arrow handles internally. 3. DatafusionReduceSink.typesMatch — relax Java's tripwire to treat same-width Int (any signedness) as compatible, mirroring the existing Timestamp(any precision) tolerance. Same rationale: divergence is harmless because the sender_send coercion produces a downstream- compatible batch. Pass-rate impact: - CalciteTimechartCommandIT: 5/18 → 18/18 ✅ (+13) - CalciteTimechartPerFunctionIT: 1/12 → 1/12 (residuals are opensearch-project#21584's multi-unit-SPAN gap + variable-month TIMESTAMPADD) - Combined timechart: 6/30 → 19/30 (63%) Signed-off-by: Kai Huang <huangkaics@gmail.com>
…er_*
Adds a peephole adapter that constant-folds the exact expression shape PPL
timechart's per_second / per_minute / per_hour / per_day aggregations
produce:
TIMESTAMPDIFF(out_unit, t, TIMESTAMPADD(in_unit, n, t))
When both units are fixed-length (MICROSECOND through WEEK) and the inner
TIMESTAMPADD's base is the same RexNode as the outer TIMESTAMPDIFF's start,
the expression evaluates to the constant n * (in_ms / out_ms) regardless of
t. The adapter materializes that BIGINT literal and lets it flow through
Substrait — eliminating both PPL UDF references in one step (neither has a
Substrait extension binding).
OperatorAnnotation wrappers (AnnotatedProjectExpression introduced by
OpenSearchProjectRule) are peeled at each operand before the structural
comparison, so the wrapped inner TIMESTAMPADD remains recognizable instead
of looking like an annotation RexCall whose operator is ANNOTATED_PROJECT_EXPR.
Out of scope (call falls through unchanged):
- Variable-length out_unit / in_unit (MONTH / QUARTER / YEAR) — fold isn't
safe because milliseconds-per-month depends on which month t lands in.
testTimechartPerSecondWithVariableMonthLengths is an example: span=1M
with per_second expects different per-row results for Feb vs Oct.
- Standalone TIMESTAMPADD (not nested inside TIMESTAMPDIFF) — no adapter
registered for TIMESTAMPADD on its own.
- Non-literal unit / n operands — the peephole requires both to be string
/ integer literals.
Pass-rate impact on the subset this adapter targets (CalciteTimechartPerFunctionIT):
- Before: 6 tests blocked at "Unable to convert call TIMESTAMPADD(string, i32, ...)".
- After: 1 test now reaches client-side assertion (sql#5420 schema-string
cast), 4 tests reveal the orthogonal multi-unit SPAN limitation
(opensearch-project#21584's known gap), 1 test correctly stays blocked on
variable-month-length.
CalciteStatsCommandIT remains at 47/63 (no regression).
Signed-off-by: Kai Huang <huangkaics@gmail.com>
Resolves the 13 by-clause failures in CalciteTimechartCommandIT (5/18 → 18/18) caused by DataFusion's row_number() physical op emitting UInt64 while Calcite types ROW_NUMBER OVER as BIGINT (Int64 signed). Two coupled changes mirror the existing schema_coerce::coerce_inferred_schema bridge the parquet-scan path uses, applied at the partition-stream boundary where the analytics-engine reduce stage reads from upstream PARTIAL fragments via NativeBridge.registerPartitionStream. 1. api::register_partition_stream — coerce the producer's physical schema via schema_coerce before registering the StreamingTable and before writing the IPC bytes back to Java. The Substrait consumer plan's `ensure_schema_compatibility` (datafusion-substrait 53) strict-checks each ReadRel's base_schema against the registered table; the coercion makes Int64-declared plans bind against this table. The Java-side typesMatch tripwire then sees the coerced schema and validates incoming batches against it (next change). 2. api::sender_send — coerce incoming RecordBatches to match the sender's (now-coerced) schema via arrow::compute::cast where types differ. Without this, DataFusion's HashJoin / RepartitionExec panics with "primitive array" on as_primitive::<Int64Type>() of a UInt64 column when the producer's actual batch reaches downstream operators. cast() is the existing Arrow kernel — no-op when the schemas already match, single-cast on the mismatched columns otherwise. Same width preserves zero-copy semantics modulo the bit-reinterpret arrow handles internally. 3. DatafusionReduceSink.typesMatch — relax Java's tripwire to treat same-width Int (any signedness) as compatible, mirroring the existing Timestamp(any precision) tolerance. Same rationale: divergence is harmless because the sender_send coercion produces a downstream- compatible batch. Pass-rate impact: - CalciteTimechartCommandIT: 5/18 → 18/18 ✅ (+13) - CalciteTimechartPerFunctionIT: 1/12 → 1/12 (residuals are opensearch-project#21584's multi-unit-SPAN gap + variable-month TIMESTAMPADD) - Combined timechart: 6/30 → 19/30 (63%) Signed-off-by: Kai Huang <huangkaics@gmail.com>
Summary
Wires the PPL
span(...)scalar through the analytics-engine route to DataFusion. Repurposed per @sandeshkr419's review — the AVG / STDDEV / VAR custom-Sig collection, percentile_approx UDAF onboarding, and defensive try/catch wrappers have all been dropped. AVG / STDDEV_POP / STDDEV_SAMP / VAR_POP / VAR_SAMP now flow through Calcite'sOpenSearchAggregateReduceRuledecomposition (landed in #21645) — no hand-holding needed.The PR now contains two commits:
[Analytics Backend / DataFusion] Wire SPAN scalar for stats— adds the bucketing primitive used bystats … by span(...). Numeric span lowers toFLOOR(field/interval)*interval; time span withinterval=1lowers toDATE_TRUNC(<unit>, field). Multi-unit time spans (12h, …) fall through unchanged — separate follow-up (DataFusiondate_bin).[QA] Add self-contained StatsCommandIT mirror— 16 self-contained tests undersandbox/qa/analytics-engine-restexercising thestatsoperators reachable from this PR (SPAN, plus the broader stats family that already flows through the analytics route via [analytics-engine] Collapse AggregateDecompositionResolver into BackendPlanAdapter's adaptNode chain #21645's decomposition).The previous
[Rebase tax] Add missing FieldType importcommit is dropped — #21663 fixed it upstream.What changed since the last review
DataFusionFragmentConvertor.java:479— "We are decomposing the methods via Calcite decomposition. I think all these cases/functions here are covered."collectCustomAggregateSigs+substraitNameForCustomAggremoved.SubstraitPlanRewriter.java:94— "These changes should go away as well as they seem related to support the above decomposed methods."percentile_approxUDAF onboarding commit was dropped (including theSubstraitPlanRewriterAggregate visitor literal-lift).DataFusionAnalyticsBackendPlugin.java:316— "This can go away as well."AGG_FUNCTIONSadditions +AggregateCapabilitytype-dispatch removed.Additionally dropped (related cleanup):
PlannerImpl/FragmentConversionDriverdefensiveAssertionErrortry/catch wrappers;AggregateFunction.fromNameOrErrorcase-insensitive lookup;OpenSearchAggregateSplitRulepercentile_approxskip guard.Files
…/analytics-framework/.../spi/ScalarFunction.javaScalarFunction.SPANenum entry…/analytics-backend-datafusion/.../be/datafusion/SpanAdapter.java…/analytics-backend-datafusion/.../be/datafusion/DataFusionAnalyticsBackendPlugin.javaSpanAdapter+ScalarFunction.SPANinPROJECT_SCALAR_FUNCTIONS…/analytics-backend-datafusion/.../be/datafusion/DataFusionFragmentConvertor.javaSqlLibraryOperators.DATE_TRUNC→"date_trunc"substrait mapping…/analytics-backend-datafusion/src/main/resources/opensearch_scalar_functions.yamldate_truncso the time-span rewrite resolves through isthmus binding…/qa/analytics-engine-rest/.../analytics/qa/StatsCommandIT.javastatstests over the parquet-backedcalcsdatasetVerification
CalciteStatsCommandITagainst the analytics-engine routeConfiguration: this PR + sql#5435 (deterministic tiebreaker queries) + sql#5436 (
tests.analytics.parquet_indicestoggle). Force-routed via-Dtests.analytics.parquet_indices=true. Rebased onto currentupstream/main(post-#21650to_charrewrite, post-#21663 FieldType import fix, post-#21645 AggregateDecompositionResolver collapse).percentile_approxUDAF — intentionally dropped per Sandesh's "repurpose for span explicitly" guidance. Tests:testStatsPercentile,testStatsPercentileByNullValue,testStatsPercentileByNullValueNonNullBucket,testStatsPercentileBySpan,testStatsPercentileWhere,testStatsPercentileWithCompression,testStatsPercentileWithMin,testStatsPercentileWithNull.span(birthdate, 1y)(now returnsstringinstead oftimestampafter #21650'sto_charrewrite — separate concern). Test:testStatsTimeSpan.span(@timestamp, 12h)falls throughSpanAdapter— onlyinterval=1is rewritten toDATE_TRUNC). Test:testStatsBySpanTimeWithNullBucket.head 2 from 1returnssize != 2on the analytics path (pagination quirk to investigate). Test:testStatsWithLimit.testDisableLegacyPreferred— semantic divergence between v2 default-engine preference and the analytics route's default-on.Delta from previous review: 50 → 51 (+1 PASS) thanks to #21650 which fixed
testStatsSpanSortOnMeasure's datetime cast format divergence (sql#5420).QA mirror
./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*StatsCommandIT"— 16 / 16 pass (single-value aggregates · filter + aggregate · group-by · STDDEV_POP/SAMP · VAR_POP/SAMP · numeric SPAN).Sandbox build
./gradlew :sandbox:plugins:analytics-backend-datafusion:assemble :sandbox:plugins:analytics-engine:assemble -Dsandbox.enabled=true— passes on currentupstream/main.Reproduction
Test plan
:sandbox:plugins:analytics-backend-datafusion:assemble+:sandbox:plugins:analytics-engine:assemblepass on current upstream/main.:sandbox:qa:analytics-engine-rest:integTest --tests "*StatsCommandIT"— 16 / 16 pass.CalciteStatsCommandITagainst the rebased analytics-engine cluster — 51 / 63 pass + 1 skip + 11 fail (breakdown above).