PPL `top` and `rare` lower (via `CalciteRelNodeVisitor#visitRareTopN`) to a
`LogicalProject` containing
`ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)`. On the analytics-engine
route, `OpenSearchProjectRule.annotateExpr` treated the `RexOver` as an
ordinary `RexCall` and fell into the scalar-function viability path. That
path keys on `ScalarFunction.fromSqlOperatorWithFallback`, which returns
null for window-only aggregates like `ROW_NUMBER`, so every `top` / `rare`
query died with "No backend supports scalar function [ROW_NUMBER] among
[datafusion]" before substrait emission.
Add `EngineCapability.WINDOW` and detect `RexOver` ahead of the standard
RexCall branch in `OpenSearchProjectRule#annotateExpr`. When the child's
viable backends include a WINDOW-capable backend, wrap the call in an
`AnnotatedProjectExpression` exactly like other annotated calls — the
existing strip path's `OperatorAnnotation::unwrap` returns the original
`RexOver` unchanged, so isthmus's `RexExpressionConverter#visitOver` emits
an inline substrait `WindowFunctionInvocation` (the substrait standard
catalog already binds `ROW_NUMBER`, so DataFusion's substrait consumer
decodes it natively — no separate substrait Window rel needed).
WINDOW is intentionally coarse (one boolean per backend rather than a
per-window-function `WindowCapability`). The substrait standard catalog
already constrains which window aggregates the backend's substrait
consumer can decode; a runtime decode failure is a clearer error than a
duplicated registry split between SPI and backend. The existing
`AggregateFunction` / `AggregateCapability` model remains right for
ordinary aggregates; once a second backend with different window support
lands, or a window-only function carries a non-standard call shape,
WINDOW can be promoted to a per-function class without disturbing the
planner-side annotation flow.
Result on `CalciteTopCommandIT` / `CalciteRareCommandIT` against the
force-routed analytics-engine path:
- `CalciteTopCommandIT`: 0/6 → 5/6 (only legacy-preferred test out-of-scope)
- `CalciteRareCommandIT`: 0/5 → 3/5 (legacy-preferred + sort-tie out-of-scope)
- combined: 0/11 → 8/11
Tests added in `ProjectRuleTests`:
- `testRowNumberOverPartitionByOrderByMarksAsWindow` — happy path
- `testRowNumberWithoutWindowCapabilityErrors` — capability-gap message
names the window function
- `testStripAnnotationsPreservesRexOver` — strip path returns plain
`LogicalProject(RexOver)` so isthmus can decode it
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Description
PPL
topandrarelower (viaCalciteRelNodeVisitor#visitRareTopNin opensearch-project/sql) to aLogicalProjectcontainingROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...). On the analytics-engine route,OpenSearchProjectRule.annotateExprtreated theRexOveras an ordinaryRexCalland fell into the scalar-function viability path. That path keys onScalarFunction.fromSqlOperatorWithFallback, which returns null for window-only aggregates likeROW_NUMBER, so everytop/rarequery died withNo backend supports scalar function [ROW_NUMBER] among [datafusion]before substrait emission.This PR adds
EngineCapability.WINDOWand detectsRexOverahead of the standardRexCallbranch inOpenSearchProjectRule.annotateExpr. When the child's viable backends include a WINDOW-capable backend, the call is wrapped in anAnnotatedProjectExpressionexactly like other annotated calls — the existing strip path'sOperatorAnnotation::unwrapreturns the originalRexOverunchanged, so isthmus'sRexExpressionConverter#visitOveremits an inline substraitWindowFunctionInvocation. The substrait standard catalog already bindsROW_NUMBER, so DataFusion's substrait consumer decodes it natively — no separate substrait Window rel needed.WINDOWis intentionally coarse (one boolean per backend rather than a per-window-functionWindowCapability). The substrait standard catalog already constrains which window aggregates the backend's substrait consumer can decode; a runtime decode failure is a clearer error than a duplicated registry split between SPI and backend. The existingAggregateFunction/AggregateCapabilitymodel remains right for ordinary aggregates; once a second backend with different window support lands, or a window-only function carries a non-standard call shape, WINDOW can be promoted to a per-function class without disturbing the planner-side annotation flow.Pass-rate impact
CalciteTopCommandIT/CalciteRareCommandITagainsttests.analytics.force_routing=true -Dtests.analytics.parquet_indices=trueon the SQL plugin'sintegTestRemote:CalciteTopCommandITCalciteRareCommandITThe 3-failure delta in the middle column comes entirely from pre-existing analytics-route gaps unrelated to window-function wiring (see below); they are closed by the companion SQL-plugin PR.
Out-of-scope follow-ups (resolved by companion PR)
The 3 failures left after this PR are not specific to
top/rare— they are pre-existing analytics-route gaps that already affect other commands. All 3 pass on the in-process Calcite path with the same query strings. They are closed by the companion SQL-plugin PR opensearch-project/sql#5433:testTopCommandLegacyFalse/testRareCommandLegacyFalse—RestUnifiedQueryAction.applyClusterOverrides(in opensearch-project/sql) only forwardedPPL_REX_MAX_MATCH_LIMITtoUnifiedQueryContext.withSettings(PPL_SYNTAX_LEGACY_PREFERRED, "false")updated the cluster setting but the analytics-engine route'sPPLQueryParserreads it from the unified context, which the override builder never populated. The companion PR refactors the override builder into aforwardClusterSettinghelper and forwardsPPL_SYNTAX_LEGACY_PREFERREDalongside the rex-limit key.testRareWithGroup— multiple states tied at the bucket count andROW_NUMBERpicked one based on insertion order, while the test expected a specific tie-broken value (same class astestStatsSortOnMeasure). The companion PR appends the rare/top field columns as secondary ASC keys to theROW_NUMBERORDER BY inCalciteRelNodeVisitor#visitRareTopN, so ties resolve deterministically across backends. This matches the OpenSearch terms-aggregation pushdown's existing_key:asctie-break — wire payload unchanged.Tests
ProjectRuleTestsadds three regressions on the new code path:testRowNumberOverPartitionByOrderByMarksAsWindow— happy pathtestRowNumberWithoutWindowCapabilityErrors— capability-gap error message names the window functiontestStripAnnotationsPreservesRexOver— strip path returns plainLogicalProject(RexOver)so isthmus can decode itMockDataFusionBackenddeclaresEngineCapability.WINDOWso existing project-rule tests continue to exercise the same backend gate.Verification
Sandbox-wide
./gradlew check -p sandbox -Dsandbox.enabled=trueis green.Issues Resolved
N/A — partial parity with PPL command coverage on the analytics-engine route. Closes the remaining 3 out-of-scope
top/raregaps together with the companion SQL-plugin PR opensearch-project/sql#5433.Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check here.