Route distinct_count_approx to APPROX_COUNT_DISTINCT - #22120
Conversation
PR Reviewer Guide 🔍(Review updated until commit 2ce8c21)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 2ce8c21 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit e2657de
Suggestions up to commit ede1e27
Suggestions up to commit 992afa1
Suggestions up to commit 3aa4738
|
3aa4738 to
992afa1
Compare
|
Persistent review updated to latest commit 992afa1 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #22120 +/- ##
============================================
- Coverage 73.44% 73.29% -0.16%
+ Complexity 75916 75813 -103
============================================
Files 6070 6070
Lines 344610 344610
Branches 49576 49576
============================================
- Hits 253095 252565 -530
- Misses 71373 71914 +541
+ Partials 20142 20131 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
992afa1 to
ede1e27
Compare
|
Persistent review updated to latest commit ede1e27 |
|
Persistent review updated to latest commit e2657de |
PPL's `distinct_count_approx(x)` UDAF is registered as a `SqlUserDefinedAggFunction` named `"APPROX_COUNT_DISTINCT"` (returning nullable BIGINT), not the Calcite `SqlStdOperatorTable.APPROX_COUNT_DISTINCT` stdop, so identity-keyed substrait dispatch never finds the `approx_distinct` binding. Extends `OpenSearchDistinctCountRule` to rewrite the UDF marker to the stdop alongside the existing `COUNT(DISTINCT x)` normalization, matched by operator name plus single-arg shape. Because `Aggregate.typeMatchesInferred` pins the new aggCall to the stdop's BIGINT NOT NULL while HepPlanner pins the replacement's row type to the original nullable shape, the rewritten aggregate is wrapped in a Project that casts the rewritten columns back to the original nullability — bridging both invariants. Tests: - `AggregateRuleTests` — two new cases covering the UDF→stdop rewrite with the cast-Project wrap, and the no-op path for an already-stdop call. - `DistinctCountApproxIT` (new sandbox QA IT) — end-to-end coverage for the group-by, aliased, and global query shapes against a parquet-backed index. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
`assertRowsEqual` compared expected `17L` / `2L` / `6L` / `9L` against the JSON-decoded response cells via `Object.equals`, but Jackson boxes counts as `Integer` when they fit in 32 bits — so `Long(17).equals(Integer(17))` is `false` and all three tests failed in CI with "expected Long<17> but was Integer<17>" even though the value was correct. Coerce both sides through `Number#longValue()` when both cells are numeric, matching the pattern already used in `CountFastPathIT`. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
e2657de to
2ce8c21
Compare
|
Persistent review updated to latest commit 2ce8c21 |
|
❌ Gradle check result for 2ce8c21: 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? |
|
❌ Gradle check result for 2ce8c21: 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? |
…lytics-engine route + review fixes Enables these PPL functions on the force-routed analytics-engine (DataFusion) path and addresses review feedback: - now()-family: NowFspAdapter handles now([fsp]) / current_timestamp / localtimestamp, where fsp is MySQL fractional-seconds precision (0-6). - rand([seed]): RandSeedAdapter. - sha2(input, bitLen): Sha2FunctionAdapter -> encode(digest(input, 'shaN'), 'hex'), with a backend fail-clear guard for unsupported bit lengths (TODO to move validation to the frontend once the handoff carries function metadata). - span() month bucketing: SpanAdapter. - strftime: os_strftime.rs. - date_format %c: zero-padded month (DatetimeCoverageIT updated to match the merged ppl/sql DateTimeFunctionIT expectations). - PlannerImpl: don't push filters below non-deterministic projects; rand() in a pushed-down predicate would be re-evaluated and draw a fresh value. Documented as a semantic-correctness guard. - DataFusionFragmentConvertor: shared Substrait preprocess entry point. - New QA ITs: MinMaxBooleanAggregationIT. Review fixes: removed the late defensive APPROX_COUNT_DISTINCT fallback in PplAggregateCallRewriter (fail loud instead); restored the Substrait-layer assessment TODO; clarified fsp in NowFspAdapter; tidied AggregateFunction. distinct_count_approx on the analytics-engine route is handled upstream by opensearch-project#22120 and is not part of this PR. Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
…lytics-engine route + review fixes (#21975) * [analytics-engine] Fix now()-family + enable now(fsp)/rand(seed)/sha2/span-month/ts-subtract/dc-approx on the analytics-engine route Closes several PPL scalar/aggregate gaps on the analytics-engine (DataFusion) route. Each was either an "Unable to convert call ..." Substrait failure or a result-rendering gap; all are verified end-to-end via the CalcitePPL*IT remote ITs on a parquet/composite (analytics-engine-routed) cluster. now()-family (CalciteNowLikeFunctionIT 12/12): - ArrowValues now converts Arrow Date32/Date64 -> LocalDate and Time{Sec,Milli,Micro,Nano} -> LocalTime so DATE/TIME results render as "uuuu-MM-dd" / "HH:mm:ss" via ExprValueUtils.fromObjectValue's existing temporal branch (mirrors how TIMESTAMP already arrives as LocalDateTime). Fixes current_date/curdate/current_time/curtime returning raw epoch-day / units-of-day integers. - UTC_TIMESTAMP/UTC_DATE/UTC_TIME enum constants + adapter registrations route to the same DataFusion builtins as their CURRENT_* equivalents (cluster runs in UTC), reusing the existing now/currentDate/currentTime adapters. - NowFspAdapter drops the optional fractional-seconds-precision arg so now(fsp)/current_timestamp(fsp)/sysdate(fsp) map to DataFusion's niladic now() instead of failing as "Unable to convert call now(i32)". Other scalar/aggregate fixes: - RandSeedAdapter drops the optional rand(seed) operand -> DataFusion random(). - Sha2FunctionAdapter raises a clear "Unsupported SHA2 algorithm [N]" for a concrete unsupported literal bit length, matching the SQL-plugin reference (CryptographicFunction) instead of surfacing a cryptic Substrait error. - SpanAdapter supports variable-length month/quarter/year buckets. - TimestampSubtractRewriter rewrites MINUS(timestamp, timestamp) to an epoch-second difference (to_unixtime), which is Substrait-convertible. - PplAggregateCallRewriter + AggregateFunction map DISTINCT_COUNT_APPROX to APPROX_COUNT_DISTINCT (dc/distinct_count approx form). Adds unit tests for the new adapters/rewriter and ArrowValues DATE/TIME conversion; updates the SHA2 adapter test to assert the clear-error behavior. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Address review: RAND seed fail-clear, shared Substrait preprocess, adapter arity bounds, span hardening, SHA2 message Review-driven hardening on top of the initial PR: - RandSeedAdapter: stop silently dropping the rand(seed) operand (which turned a deterministic seeded call into a non-deterministic one). Niladic rand() still maps to DataFusion random(); seeded rand(seed) now fails with a clear unsupported-shape error until a seeded random is available on the backend. - DataFusionFragmentConvertor: extract preprocessForSubstrait() and call it from both convertToSubstrait and convertStandalone so TimestampSubtractRewriter (and any future rewriter) runs on the wrapper/partial-aggregate path too, not only the top-level fragment path. - NowFspAdapter / RandSeedAdapter: only normalize the valid 0-arg / 1-arg shapes; leave unexpected arities untouched instead of inventing a valid call. - SpanAdapter: guard the month and second interval multiplications with Math.multiplyExact (clear "interval is too large" error on overflow), and use floored modulo/division for the month-index bucketing so pre-1970 (negative month index) timestamps snap down to the correct bucket start. - Sha2FunctionAdapter: list the supported bit lengths (224, 256, 384, 512) in the unsupported-algorithm error. Tests: add NowFsp unexpected-arity test, RAND seeded-reject test, span month-bucket + overflow tests; update SHA2 test to assert the supported-values message. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix grouped distinct_count_approx nullability + make TimestampSubtractRewriter identity-preserving Two correctness fixes found while verifying the review round: - PplAggregateCallRewriter (DISTINCT_COUNT_APPROX): pin the remapped aggregate's explicit return type to NOT NULL BIGINT. APPROX_COUNT_DISTINCT is a count, so its return-type inference is BIGINT NOT NULL; reusing the PPL call's nullable BIGINT left the declared and inferred types disagreeing and tripped Calcite's validity assertion ("aggCall type BIGINT vs inferred BIGINT NOT NULL") on the grouped two-phase path (e.g. `stats distinct_count_approx(x) by g`). This was a latent bug in the un-grouped-only test coverage; the grouped form is now verified green. - TimestampSubtractRewriter: make it a true identity no-op for plans without a MINUS(timestamp, timestamp). Previously it walked every RelNode and called RelNode.accept(RexShuttle) unconditionally, which re-derives expression / aggCall types and can flip a cached nullable BIGINT to BIGINT NOT NULL — breaking unrelated shapes once the shared preprocessing pipeline runs it on the two-phase aggregate path. It now detects the target shape first (read-only visitors) and only applies the rewriting RexShuttle to nodes that actually contain it, returning the original object otherwise. Adds identity (assertSame) regression tests. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Drop TimestampSubtractRewriter — superseded by upstream MinusAdapter (#21978) Upstream #21978 added MinusAdapter, the proper named ScalarFunctionAdapter for timestamp/date subtraction (t1 - t2 -> from_unixtime(to_unixtime(t1) - to_unixtime(t2))). It also coordinates with WidthBucketAdapter by deliberately leaving the MINUS(MAX OVER(), MIN OVER()) binning shape untouched. Our pre-Substrait TimestampSubtractRewriter rewrote every MINUS(timestamp, timestamp) including that binning shape, so keeping both risked clobbering the binning path. Remove the rewriter (and its test); MinusAdapter fully covers the case. The shared preprocessForSubstrait helper is retained for the remaining rewriters. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Bind distinct_count_approx after SQL-plugin emits APPROX_COUNT_DISTINCT (#5525) sql#5525 changed PPLBuiltinOperators.DISTINCT_COUNT_APPROX to emit a user-defined SqlAggFunction NAMED "APPROX_COUNT_DISTINCT" (DistinctCountApproxLogicalAggFunction). isthmus binds aggregate sigs by operator identity, not name, so this custom op has no substrait sig and grouped/ungrouped `stats distinct_count_approx(x)` fails with "Unable to find binding for call APPROX_COUNT_DISTINCT". (The `dc`->COUNT(DISTINCT) path works via OpenSearchDistinctCountRule/#22013; the named UDAF path does not.) Re-key the PplAggregateCallRewriter case from the old name DISTINCT_COUNT_APPROX to APPROX_COUNT_DISTINCT and remap the PPL marker to stock SqlStdOperatorTable.APPROX_COUNT_DISTINCT (which ADDITIONAL_AGGREGATE_SIGS binds to DataFusion approx_distinct), guarding against the stock operator itself. Remove the now-dead DISTINCT_COUNT_APPROX alias in AggregateFunction.fromNameOrError — the enum constant is APPROX_COUNT_DISTINCT, which valueOf resolves directly (addresses review comment). Verified: CalcitePPLAggregationIT.testCountDistinctApprox + testCountDistinctApproxWithAlias pass on a force-routed analytics-engine cluster. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Don't push filters below non-deterministic projects (fixes rand() predicates) `eval r = rand() | where r > 0` failed with "Comparison performance-delegation requires (RexInputRef, RexLiteral); got RAND()". Calcite's stock FILTER_PROJECT_TRANSPOSE only guards against window functions (!containsOver()), so it pushed the filter below the rand() project, inlining RAND() into the predicate — turning a delegatable ($ref > literal) comparison into RAND() > literal on the scan, which Lucene performance-delegation cannot serialize (and which must stay on the single in-memory engine, since each backend would draw different values). Replace the stock rule with FILTER_PROJECT_TRANSPOSE_DETERMINISTIC: a FilterProjectTransposeRule configured to refuse the transpose when the Project computes any non-deterministic expression (RexUtil.isDeterministic). Keeping the Filter above the Project preserves the clean ($ref > literal) shape evaluated in memory. Verified: CalcitePPLBuiltinFunctionIT.testRand passes on a force-routed analytics-engine cluster. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Restore distinct_count_approx binding on the analytics-engine route Validation requested in PR review (sandeshkr419) found that sql#5525 alone does NOT make `stats distinct_count_approx(x)` work on the analytics-engine route, so the engine-side handling removed in the prior commit is still required. On a fresh, force-routed cluster with sql#5525 deployed, CalcitePPLAggregationIT testCountDistinctApprox + testCountDistinctApproxWithAlias fail in two stages: 1. Planner stage: OpenSearchAggregateRule.resolveViableBackendsForCall reads aggCall.getAggregation().getName(), which is still "DISTINCT_COUNT_APPROX" (the DistinctCountApproxLogicalAggFunction marker). fromNameOrError then throws "No enum constant ...AggregateFunction.DISTINCT_COUNT_APPROX". 2. Substrait stage: the PplAggregateCallRewriter case keyed only on "APPROX_COUNT_DISTINCT" never matched the marker, so the unbound op reached isthmus -> "Unable to find binding for call DISTINCT_COUNT_APPROX($1)". sql#5525 only sets the substrait-emission name to APPROX_COUNT_DISTINCT, which is a later stage than both of the above, so the marker's runtime name still reaches them. Fixes: - Restore the DISTINCT_COUNT_APPROX -> APPROX_COUNT_DISTINCT alias in AggregateFunction.fromNameOrError (covers the planner path). - Match both "DISTINCT_COUNT_APPROX" and "APPROX_COUNT_DISTINCT" in the PplAggregateCallRewriter case (covers the substrait-binding path); keep the guard that skips the already-bound stock operator. - Add a fromNameOrError alias unit test. Verified on a force-routed analytics-engine cluster (parquet-backed indices, AE-routed): both tests pass; the surrounding 4-class IT sweep is unchanged at 6 pre-existing failures (date_format/strftime/percentile/nested-field), i.e. no regressions. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Add qa-module ITs for distinct_count_approx + min/max(boolean) Engine-side end-to-end regression coverage for the two analytics-engine binding fixes in this PR, run against the production /_plugins/_ppl surface on the qa-module's analytics-routed cluster (mirrors the SQL-repo CalcitePPLAggregationIT cases that proved the fixes): - DistinctCountApproxAggregationIT: grouped / aliased / dc-shorthand / ungrouped distinct_count_approx (guards the planner enum alias + rewriter rebind). - MinMaxBooleanAggregationIT: ungrouped / grouped / mixed boolean+string min/max (guards the boolean min/max substrait overload). Each IT creates its own parquet-backed (composite/parquet + lucene secondary) index, bulk-ingests a small fixed dataset, and asserts oracle values. All 7 tests pass (4 + 3). Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix date_format %c month padding; correct stale RAND comments Two small follow-ups surfaced while verifying the analytics-engine route: - date_format/strftime %c now emits the zero-padded month (Jan -> "01"), matching the PPL reference (SQL-plugin DateTimeFormatterUtil maps %c -> "MM" in date mode), which differs from stock MySQL's no-leading-zero %c. Time mode keeps the reference's single-"0" literal. Fixes the %c portion of CalciteDateTimeFunctionIT.testDateFormat and updates the Rust token-set unit test expectation accordingly. (The test's %U/%u/%V/%v week-number padding is a separate, unrelated date-format issue left for a follow-up.) - Correct two stale comments that described the old "drop the seed" RAND behavior; the adapter now rejects RAND(seed) with a clear error (addresses review feedback). No behavior change — comment/javadoc only. Verified on a force-routed analytics-engine cluster: date_format(ts,'%c') returns "01"; the now/sha2/dc/min-max ITs remain green (no regressions). Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Drop DISTINCT_COUNT_APPROX enum alias per review Remove the DISTINCT_COUNT_APPROX -> APPROX_COUNT_DISTINCT alias in AggregateFunction.fromNameOrError (and its unit test), and key the PplAggregateCallRewriter case on "APPROX_COUNT_DISTINCT" only, per @sandeshkr419's review. The SQL plugin (sql#5525) emits the operator named APPROX_COUNT_DISTINCT, which the enum resolves directly, so the legacy-name alias is no longer needed. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Address review: bulk NDJSON content-type, dc comments, error wording Review follow-ups (sandeshkr419): - QA ITs: send _bulk as application/x-ndjson via the per-index endpoint with bare {"index": {}} action metadata, matching the existing CountFastPathIT pattern. setJsonEntity alone defaults to application/json, which the bulk endpoint rejects. - Clarify the distinct_count_approx marker naming across layers: current runtime name is APPROX_COUNT_DISTINCT (sql#5525), DISTINCT_COUNT_APPROX is the legacy/defensive spelling; OpenSearchDistinctCountRule is the primary planner rewrite and PplAggregateCallRewriter is a late Substrait-emission defensive fallback. - Document the phase-specific nullability handling: the early planner rewrite preserves the marker's nullable type (LogicalAggregate.copy re-validates row type); the late Substrait rewrite pins NOT NULL (Calcite validates against the stock op's inferred type). - SPAN overflow message now includes the operands. - Drop "yet" from the seeded-RAND error so it doesn't imply imminent support. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Empty commit to retrigger CI No code change. The prior gradle-check failed in the Jenkins-trigger step (jq parse error polling the Jenkins API, 10 retries, empty result) — a transient CI-infra glitch unrelated to this PR's changes. Re-running checks. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix DatetimeCoverageIT %c assertions to match zero-padded month The date_format %c code change (no-leading-zero -> zero-padded "MM") aligns the analytics-engine Rust UDF with the legacy PPL contract: DateTimeFormatterUtil.DATE_HANDLERS maps %c -> "MM" (date_format dispatches through DATE_HANDLERS), so January renders as "01". The Rust code + unit test were updated for this, but the two DatetimeCoverageIT assertions still expected the old unpadded "1" — the gradle-check / sandbox-check failures. Update both assertions to "01", matching the canonical merged ITs (ppl/DateTimeFunctionIT and sql/DateTimeFunctionIT both assert "Sat Jan 01 31st ..." for the same spec). Test-only. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Enable now()/rand(seed)/sha2/span-month on the analytics-engine route + review fixes Enables these PPL functions on the force-routed analytics-engine (DataFusion) path and addresses review feedback: - now()-family: NowFspAdapter handles now([fsp]) / current_timestamp / localtimestamp, where fsp is MySQL fractional-seconds precision (0-6). - rand([seed]): RandSeedAdapter. - sha2(input, bitLen): Sha2FunctionAdapter -> encode(digest(input, 'shaN'), 'hex'), with a backend fail-clear guard for unsupported bit lengths (TODO to move validation to the frontend once the handoff carries function metadata). - span() month bucketing: SpanAdapter. - strftime: os_strftime.rs. - date_format %c: zero-padded month (DatetimeCoverageIT updated to match the merged ppl/sql DateTimeFunctionIT expectations). - PlannerImpl: don't push filters below non-deterministic projects; rand() in a pushed-down predicate would be re-evaluated and draw a fresh value. Documented as a semantic-correctness guard. - DataFusionFragmentConvertor: shared Substrait preprocess entry point. - New QA ITs: MinMaxBooleanAggregationIT. Review fixes: removed the late defensive APPROX_COUNT_DISTINCT fallback in PplAggregateCallRewriter (fail loud instead); restored the Substrait-layer assessment TODO; clarified fsp in NowFspAdapter; tidied AggregateFunction. distinct_count_approx on the analytics-engine route is handled upstream by #22120 and is not part of this PR. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Add 2-shard reduce coverage for boolean min/max Addresses review feedback to cover the multi-shard case for boolean min/max. min(flag)/max(flag) over the merge_coverage dataset's boolean field are added as golden cases in the TwoShardReduceTestCase suite (#21951 convention), so they run at 1 shard and 2 shards with a differential equality check plus a pinned golden, exercising the two-phase per-shard-partial then coordinator-merge reduce path. The merge_coverage flag field has 15 false and 15 true rows, so min=false and max=true regardless of shard layout. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> --------- Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
…ect#22120) * Route distinct_count_approx to APPROX_COUNT_DISTINCT PPL's `distinct_count_approx(x)` UDAF is registered as a `SqlUserDefinedAggFunction` named `"APPROX_COUNT_DISTINCT"` (returning nullable BIGINT), not the Calcite `SqlStdOperatorTable.APPROX_COUNT_DISTINCT` stdop, so identity-keyed substrait dispatch never finds the `approx_distinct` binding. Extends `OpenSearchDistinctCountRule` to rewrite the UDF marker to the stdop alongside the existing `COUNT(DISTINCT x)` normalization, matched by operator name plus single-arg shape. Because `Aggregate.typeMatchesInferred` pins the new aggCall to the stdop's BIGINT NOT NULL while HepPlanner pins the replacement's row type to the original nullable shape, the rewritten aggregate is wrapped in a Project that casts the rewritten columns back to the original nullability — bridging both invariants. Tests: - `AggregateRuleTests` — two new cases covering the UDF→stdop rewrite with the cast-Project wrap, and the no-op path for an already-stdop call. - `DistinctCountApproxIT` (new sandbox QA IT) — end-to-end coverage for the group-by, aliased, and global query shapes against a parquet-backed index. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com> * Compare numeric cells by value in DistinctCountApproxIT `assertRowsEqual` compared expected `17L` / `2L` / `6L` / `9L` against the JSON-decoded response cells via `Object.equals`, but Jackson boxes counts as `Integer` when they fit in 32 bits — so `Long(17).equals(Integer(17))` is `false` and all three tests failed in CI with "expected Long<17> but was Integer<17>" even though the value was correct. Coerce both sides through `Number#longValue()` when both cells are numeric, matching the pattern already used in `CountFastPathIT`. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com> --------- Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
…lytics-engine route + review fixes (opensearch-project#21975) * [analytics-engine] Fix now()-family + enable now(fsp)/rand(seed)/sha2/span-month/ts-subtract/dc-approx on the analytics-engine route Closes several PPL scalar/aggregate gaps on the analytics-engine (DataFusion) route. Each was either an "Unable to convert call ..." Substrait failure or a result-rendering gap; all are verified end-to-end via the CalcitePPL*IT remote ITs on a parquet/composite (analytics-engine-routed) cluster. now()-family (CalciteNowLikeFunctionIT 12/12): - ArrowValues now converts Arrow Date32/Date64 -> LocalDate and Time{Sec,Milli,Micro,Nano} -> LocalTime so DATE/TIME results render as "uuuu-MM-dd" / "HH:mm:ss" via ExprValueUtils.fromObjectValue's existing temporal branch (mirrors how TIMESTAMP already arrives as LocalDateTime). Fixes current_date/curdate/current_time/curtime returning raw epoch-day / units-of-day integers. - UTC_TIMESTAMP/UTC_DATE/UTC_TIME enum constants + adapter registrations route to the same DataFusion builtins as their CURRENT_* equivalents (cluster runs in UTC), reusing the existing now/currentDate/currentTime adapters. - NowFspAdapter drops the optional fractional-seconds-precision arg so now(fsp)/current_timestamp(fsp)/sysdate(fsp) map to DataFusion's niladic now() instead of failing as "Unable to convert call now(i32)". Other scalar/aggregate fixes: - RandSeedAdapter drops the optional rand(seed) operand -> DataFusion random(). - Sha2FunctionAdapter raises a clear "Unsupported SHA2 algorithm [N]" for a concrete unsupported literal bit length, matching the SQL-plugin reference (CryptographicFunction) instead of surfacing a cryptic Substrait error. - SpanAdapter supports variable-length month/quarter/year buckets. - TimestampSubtractRewriter rewrites MINUS(timestamp, timestamp) to an epoch-second difference (to_unixtime), which is Substrait-convertible. - PplAggregateCallRewriter + AggregateFunction map DISTINCT_COUNT_APPROX to APPROX_COUNT_DISTINCT (dc/distinct_count approx form). Adds unit tests for the new adapters/rewriter and ArrowValues DATE/TIME conversion; updates the SHA2 adapter test to assert the clear-error behavior. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Address review: RAND seed fail-clear, shared Substrait preprocess, adapter arity bounds, span hardening, SHA2 message Review-driven hardening on top of the initial PR: - RandSeedAdapter: stop silently dropping the rand(seed) operand (which turned a deterministic seeded call into a non-deterministic one). Niladic rand() still maps to DataFusion random(); seeded rand(seed) now fails with a clear unsupported-shape error until a seeded random is available on the backend. - DataFusionFragmentConvertor: extract preprocessForSubstrait() and call it from both convertToSubstrait and convertStandalone so TimestampSubtractRewriter (and any future rewriter) runs on the wrapper/partial-aggregate path too, not only the top-level fragment path. - NowFspAdapter / RandSeedAdapter: only normalize the valid 0-arg / 1-arg shapes; leave unexpected arities untouched instead of inventing a valid call. - SpanAdapter: guard the month and second interval multiplications with Math.multiplyExact (clear "interval is too large" error on overflow), and use floored modulo/division for the month-index bucketing so pre-1970 (negative month index) timestamps snap down to the correct bucket start. - Sha2FunctionAdapter: list the supported bit lengths (224, 256, 384, 512) in the unsupported-algorithm error. Tests: add NowFsp unexpected-arity test, RAND seeded-reject test, span month-bucket + overflow tests; update SHA2 test to assert the supported-values message. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix grouped distinct_count_approx nullability + make TimestampSubtractRewriter identity-preserving Two correctness fixes found while verifying the review round: - PplAggregateCallRewriter (DISTINCT_COUNT_APPROX): pin the remapped aggregate's explicit return type to NOT NULL BIGINT. APPROX_COUNT_DISTINCT is a count, so its return-type inference is BIGINT NOT NULL; reusing the PPL call's nullable BIGINT left the declared and inferred types disagreeing and tripped Calcite's validity assertion ("aggCall type BIGINT vs inferred BIGINT NOT NULL") on the grouped two-phase path (e.g. `stats distinct_count_approx(x) by g`). This was a latent bug in the un-grouped-only test coverage; the grouped form is now verified green. - TimestampSubtractRewriter: make it a true identity no-op for plans without a MINUS(timestamp, timestamp). Previously it walked every RelNode and called RelNode.accept(RexShuttle) unconditionally, which re-derives expression / aggCall types and can flip a cached nullable BIGINT to BIGINT NOT NULL — breaking unrelated shapes once the shared preprocessing pipeline runs it on the two-phase aggregate path. It now detects the target shape first (read-only visitors) and only applies the rewriting RexShuttle to nodes that actually contain it, returning the original object otherwise. Adds identity (assertSame) regression tests. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Drop TimestampSubtractRewriter — superseded by upstream MinusAdapter (opensearch-project#21978) Upstream opensearch-project#21978 added MinusAdapter, the proper named ScalarFunctionAdapter for timestamp/date subtraction (t1 - t2 -> from_unixtime(to_unixtime(t1) - to_unixtime(t2))). It also coordinates with WidthBucketAdapter by deliberately leaving the MINUS(MAX OVER(), MIN OVER()) binning shape untouched. Our pre-Substrait TimestampSubtractRewriter rewrote every MINUS(timestamp, timestamp) including that binning shape, so keeping both risked clobbering the binning path. Remove the rewriter (and its test); MinusAdapter fully covers the case. The shared preprocessForSubstrait helper is retained for the remaining rewriters. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Bind distinct_count_approx after SQL-plugin emits APPROX_COUNT_DISTINCT (opensearch-project#5525) sql#5525 changed PPLBuiltinOperators.DISTINCT_COUNT_APPROX to emit a user-defined SqlAggFunction NAMED "APPROX_COUNT_DISTINCT" (DistinctCountApproxLogicalAggFunction). isthmus binds aggregate sigs by operator identity, not name, so this custom op has no substrait sig and grouped/ungrouped `stats distinct_count_approx(x)` fails with "Unable to find binding for call APPROX_COUNT_DISTINCT". (The `dc`->COUNT(DISTINCT) path works via OpenSearchDistinctCountRule/opensearch-project#22013; the named UDAF path does not.) Re-key the PplAggregateCallRewriter case from the old name DISTINCT_COUNT_APPROX to APPROX_COUNT_DISTINCT and remap the PPL marker to stock SqlStdOperatorTable.APPROX_COUNT_DISTINCT (which ADDITIONAL_AGGREGATE_SIGS binds to DataFusion approx_distinct), guarding against the stock operator itself. Remove the now-dead DISTINCT_COUNT_APPROX alias in AggregateFunction.fromNameOrError — the enum constant is APPROX_COUNT_DISTINCT, which valueOf resolves directly (addresses review comment). Verified: CalcitePPLAggregationIT.testCountDistinctApprox + testCountDistinctApproxWithAlias pass on a force-routed analytics-engine cluster. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Don't push filters below non-deterministic projects (fixes rand() predicates) `eval r = rand() | where r > 0` failed with "Comparison performance-delegation requires (RexInputRef, RexLiteral); got RAND()". Calcite's stock FILTER_PROJECT_TRANSPOSE only guards against window functions (!containsOver()), so it pushed the filter below the rand() project, inlining RAND() into the predicate — turning a delegatable ($ref > literal) comparison into RAND() > literal on the scan, which Lucene performance-delegation cannot serialize (and which must stay on the single in-memory engine, since each backend would draw different values). Replace the stock rule with FILTER_PROJECT_TRANSPOSE_DETERMINISTIC: a FilterProjectTransposeRule configured to refuse the transpose when the Project computes any non-deterministic expression (RexUtil.isDeterministic). Keeping the Filter above the Project preserves the clean ($ref > literal) shape evaluated in memory. Verified: CalcitePPLBuiltinFunctionIT.testRand passes on a force-routed analytics-engine cluster. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Restore distinct_count_approx binding on the analytics-engine route Validation requested in PR review (sandeshkr419) found that sql#5525 alone does NOT make `stats distinct_count_approx(x)` work on the analytics-engine route, so the engine-side handling removed in the prior commit is still required. On a fresh, force-routed cluster with sql#5525 deployed, CalcitePPLAggregationIT testCountDistinctApprox + testCountDistinctApproxWithAlias fail in two stages: 1. Planner stage: OpenSearchAggregateRule.resolveViableBackendsForCall reads aggCall.getAggregation().getName(), which is still "DISTINCT_COUNT_APPROX" (the DistinctCountApproxLogicalAggFunction marker). fromNameOrError then throws "No enum constant ...AggregateFunction.DISTINCT_COUNT_APPROX". 2. Substrait stage: the PplAggregateCallRewriter case keyed only on "APPROX_COUNT_DISTINCT" never matched the marker, so the unbound op reached isthmus -> "Unable to find binding for call DISTINCT_COUNT_APPROX($1)". sql#5525 only sets the substrait-emission name to APPROX_COUNT_DISTINCT, which is a later stage than both of the above, so the marker's runtime name still reaches them. Fixes: - Restore the DISTINCT_COUNT_APPROX -> APPROX_COUNT_DISTINCT alias in AggregateFunction.fromNameOrError (covers the planner path). - Match both "DISTINCT_COUNT_APPROX" and "APPROX_COUNT_DISTINCT" in the PplAggregateCallRewriter case (covers the substrait-binding path); keep the guard that skips the already-bound stock operator. - Add a fromNameOrError alias unit test. Verified on a force-routed analytics-engine cluster (parquet-backed indices, AE-routed): both tests pass; the surrounding 4-class IT sweep is unchanged at 6 pre-existing failures (date_format/strftime/percentile/nested-field), i.e. no regressions. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Add qa-module ITs for distinct_count_approx + min/max(boolean) Engine-side end-to-end regression coverage for the two analytics-engine binding fixes in this PR, run against the production /_plugins/_ppl surface on the qa-module's analytics-routed cluster (mirrors the SQL-repo CalcitePPLAggregationIT cases that proved the fixes): - DistinctCountApproxAggregationIT: grouped / aliased / dc-shorthand / ungrouped distinct_count_approx (guards the planner enum alias + rewriter rebind). - MinMaxBooleanAggregationIT: ungrouped / grouped / mixed boolean+string min/max (guards the boolean min/max substrait overload). Each IT creates its own parquet-backed (composite/parquet + lucene secondary) index, bulk-ingests a small fixed dataset, and asserts oracle values. All 7 tests pass (4 + 3). Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix date_format %c month padding; correct stale RAND comments Two small follow-ups surfaced while verifying the analytics-engine route: - date_format/strftime %c now emits the zero-padded month (Jan -> "01"), matching the PPL reference (SQL-plugin DateTimeFormatterUtil maps %c -> "MM" in date mode), which differs from stock MySQL's no-leading-zero %c. Time mode keeps the reference's single-"0" literal. Fixes the %c portion of CalciteDateTimeFunctionIT.testDateFormat and updates the Rust token-set unit test expectation accordingly. (The test's %U/%u/%V/%v week-number padding is a separate, unrelated date-format issue left for a follow-up.) - Correct two stale comments that described the old "drop the seed" RAND behavior; the adapter now rejects RAND(seed) with a clear error (addresses review feedback). No behavior change — comment/javadoc only. Verified on a force-routed analytics-engine cluster: date_format(ts,'%c') returns "01"; the now/sha2/dc/min-max ITs remain green (no regressions). Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Drop DISTINCT_COUNT_APPROX enum alias per review Remove the DISTINCT_COUNT_APPROX -> APPROX_COUNT_DISTINCT alias in AggregateFunction.fromNameOrError (and its unit test), and key the PplAggregateCallRewriter case on "APPROX_COUNT_DISTINCT" only, per @sandeshkr419's review. The SQL plugin (sql#5525) emits the operator named APPROX_COUNT_DISTINCT, which the enum resolves directly, so the legacy-name alias is no longer needed. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Address review: bulk NDJSON content-type, dc comments, error wording Review follow-ups (sandeshkr419): - QA ITs: send _bulk as application/x-ndjson via the per-index endpoint with bare {"index": {}} action metadata, matching the existing CountFastPathIT pattern. setJsonEntity alone defaults to application/json, which the bulk endpoint rejects. - Clarify the distinct_count_approx marker naming across layers: current runtime name is APPROX_COUNT_DISTINCT (sql#5525), DISTINCT_COUNT_APPROX is the legacy/defensive spelling; OpenSearchDistinctCountRule is the primary planner rewrite and PplAggregateCallRewriter is a late Substrait-emission defensive fallback. - Document the phase-specific nullability handling: the early planner rewrite preserves the marker's nullable type (LogicalAggregate.copy re-validates row type); the late Substrait rewrite pins NOT NULL (Calcite validates against the stock op's inferred type). - SPAN overflow message now includes the operands. - Drop "yet" from the seeded-RAND error so it doesn't imply imminent support. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Empty commit to retrigger CI No code change. The prior gradle-check failed in the Jenkins-trigger step (jq parse error polling the Jenkins API, 10 retries, empty result) — a transient CI-infra glitch unrelated to this PR's changes. Re-running checks. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix DatetimeCoverageIT %c assertions to match zero-padded month The date_format %c code change (no-leading-zero -> zero-padded "MM") aligns the analytics-engine Rust UDF with the legacy PPL contract: DateTimeFormatterUtil.DATE_HANDLERS maps %c -> "MM" (date_format dispatches through DATE_HANDLERS), so January renders as "01". The Rust code + unit test were updated for this, but the two DatetimeCoverageIT assertions still expected the old unpadded "1" — the gradle-check / sandbox-check failures. Update both assertions to "01", matching the canonical merged ITs (ppl/DateTimeFunctionIT and sql/DateTimeFunctionIT both assert "Sat Jan 01 31st ..." for the same spec). Test-only. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Enable now()/rand(seed)/sha2/span-month on the analytics-engine route + review fixes Enables these PPL functions on the force-routed analytics-engine (DataFusion) path and addresses review feedback: - now()-family: NowFspAdapter handles now([fsp]) / current_timestamp / localtimestamp, where fsp is MySQL fractional-seconds precision (0-6). - rand([seed]): RandSeedAdapter. - sha2(input, bitLen): Sha2FunctionAdapter -> encode(digest(input, 'shaN'), 'hex'), with a backend fail-clear guard for unsupported bit lengths (TODO to move validation to the frontend once the handoff carries function metadata). - span() month bucketing: SpanAdapter. - strftime: os_strftime.rs. - date_format %c: zero-padded month (DatetimeCoverageIT updated to match the merged ppl/sql DateTimeFunctionIT expectations). - PlannerImpl: don't push filters below non-deterministic projects; rand() in a pushed-down predicate would be re-evaluated and draw a fresh value. Documented as a semantic-correctness guard. - DataFusionFragmentConvertor: shared Substrait preprocess entry point. - New QA ITs: MinMaxBooleanAggregationIT. Review fixes: removed the late defensive APPROX_COUNT_DISTINCT fallback in PplAggregateCallRewriter (fail loud instead); restored the Substrait-layer assessment TODO; clarified fsp in NowFspAdapter; tidied AggregateFunction. distinct_count_approx on the analytics-engine route is handled upstream by opensearch-project#22120 and is not part of this PR. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Add 2-shard reduce coverage for boolean min/max Addresses review feedback to cover the multi-shard case for boolean min/max. min(flag)/max(flag) over the merge_coverage dataset's boolean field are added as golden cases in the TwoShardReduceTestCase suite (opensearch-project#21951 convention), so they run at 1 shard and 2 shards with a differential equality check plus a pinned golden, exercising the two-phase per-shard-partial then coordinator-merge reduce path. The merge_coverage flag field has 15 false and 15 true rows, so min=false and max=true regardless of shard layout. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> --------- Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
…ect#22120) * Route distinct_count_approx to APPROX_COUNT_DISTINCT PPL's `distinct_count_approx(x)` UDAF is registered as a `SqlUserDefinedAggFunction` named `"APPROX_COUNT_DISTINCT"` (returning nullable BIGINT), not the Calcite `SqlStdOperatorTable.APPROX_COUNT_DISTINCT` stdop, so identity-keyed substrait dispatch never finds the `approx_distinct` binding. Extends `OpenSearchDistinctCountRule` to rewrite the UDF marker to the stdop alongside the existing `COUNT(DISTINCT x)` normalization, matched by operator name plus single-arg shape. Because `Aggregate.typeMatchesInferred` pins the new aggCall to the stdop's BIGINT NOT NULL while HepPlanner pins the replacement's row type to the original nullable shape, the rewritten aggregate is wrapped in a Project that casts the rewritten columns back to the original nullability — bridging both invariants. Tests: - `AggregateRuleTests` — two new cases covering the UDF→stdop rewrite with the cast-Project wrap, and the no-op path for an already-stdop call. - `DistinctCountApproxIT` (new sandbox QA IT) — end-to-end coverage for the group-by, aliased, and global query shapes against a parquet-backed index. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com> * Compare numeric cells by value in DistinctCountApproxIT `assertRowsEqual` compared expected `17L` / `2L` / `6L` / `9L` against the JSON-decoded response cells via `Object.equals`, but Jackson boxes counts as `Integer` when they fit in 32 bits — so `Long(17).equals(Integer(17))` is `false` and all three tests failed in CI with "expected Long<17> but was Integer<17>" even though the value was correct. Coerce both sides through `Number#longValue()` when both cells are numeric, matching the pattern already used in `CountFastPathIT`. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com> --------- Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
…lytics-engine route + review fixes (opensearch-project#21975) * [analytics-engine] Fix now()-family + enable now(fsp)/rand(seed)/sha2/span-month/ts-subtract/dc-approx on the analytics-engine route Closes several PPL scalar/aggregate gaps on the analytics-engine (DataFusion) route. Each was either an "Unable to convert call ..." Substrait failure or a result-rendering gap; all are verified end-to-end via the CalcitePPL*IT remote ITs on a parquet/composite (analytics-engine-routed) cluster. now()-family (CalciteNowLikeFunctionIT 12/12): - ArrowValues now converts Arrow Date32/Date64 -> LocalDate and Time{Sec,Milli,Micro,Nano} -> LocalTime so DATE/TIME results render as "uuuu-MM-dd" / "HH:mm:ss" via ExprValueUtils.fromObjectValue's existing temporal branch (mirrors how TIMESTAMP already arrives as LocalDateTime). Fixes current_date/curdate/current_time/curtime returning raw epoch-day / units-of-day integers. - UTC_TIMESTAMP/UTC_DATE/UTC_TIME enum constants + adapter registrations route to the same DataFusion builtins as their CURRENT_* equivalents (cluster runs in UTC), reusing the existing now/currentDate/currentTime adapters. - NowFspAdapter drops the optional fractional-seconds-precision arg so now(fsp)/current_timestamp(fsp)/sysdate(fsp) map to DataFusion's niladic now() instead of failing as "Unable to convert call now(i32)". Other scalar/aggregate fixes: - RandSeedAdapter drops the optional rand(seed) operand -> DataFusion random(). - Sha2FunctionAdapter raises a clear "Unsupported SHA2 algorithm [N]" for a concrete unsupported literal bit length, matching the SQL-plugin reference (CryptographicFunction) instead of surfacing a cryptic Substrait error. - SpanAdapter supports variable-length month/quarter/year buckets. - TimestampSubtractRewriter rewrites MINUS(timestamp, timestamp) to an epoch-second difference (to_unixtime), which is Substrait-convertible. - PplAggregateCallRewriter + AggregateFunction map DISTINCT_COUNT_APPROX to APPROX_COUNT_DISTINCT (dc/distinct_count approx form). Adds unit tests for the new adapters/rewriter and ArrowValues DATE/TIME conversion; updates the SHA2 adapter test to assert the clear-error behavior. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Address review: RAND seed fail-clear, shared Substrait preprocess, adapter arity bounds, span hardening, SHA2 message Review-driven hardening on top of the initial PR: - RandSeedAdapter: stop silently dropping the rand(seed) operand (which turned a deterministic seeded call into a non-deterministic one). Niladic rand() still maps to DataFusion random(); seeded rand(seed) now fails with a clear unsupported-shape error until a seeded random is available on the backend. - DataFusionFragmentConvertor: extract preprocessForSubstrait() and call it from both convertToSubstrait and convertStandalone so TimestampSubtractRewriter (and any future rewriter) runs on the wrapper/partial-aggregate path too, not only the top-level fragment path. - NowFspAdapter / RandSeedAdapter: only normalize the valid 0-arg / 1-arg shapes; leave unexpected arities untouched instead of inventing a valid call. - SpanAdapter: guard the month and second interval multiplications with Math.multiplyExact (clear "interval is too large" error on overflow), and use floored modulo/division for the month-index bucketing so pre-1970 (negative month index) timestamps snap down to the correct bucket start. - Sha2FunctionAdapter: list the supported bit lengths (224, 256, 384, 512) in the unsupported-algorithm error. Tests: add NowFsp unexpected-arity test, RAND seeded-reject test, span month-bucket + overflow tests; update SHA2 test to assert the supported-values message. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix grouped distinct_count_approx nullability + make TimestampSubtractRewriter identity-preserving Two correctness fixes found while verifying the review round: - PplAggregateCallRewriter (DISTINCT_COUNT_APPROX): pin the remapped aggregate's explicit return type to NOT NULL BIGINT. APPROX_COUNT_DISTINCT is a count, so its return-type inference is BIGINT NOT NULL; reusing the PPL call's nullable BIGINT left the declared and inferred types disagreeing and tripped Calcite's validity assertion ("aggCall type BIGINT vs inferred BIGINT NOT NULL") on the grouped two-phase path (e.g. `stats distinct_count_approx(x) by g`). This was a latent bug in the un-grouped-only test coverage; the grouped form is now verified green. - TimestampSubtractRewriter: make it a true identity no-op for plans without a MINUS(timestamp, timestamp). Previously it walked every RelNode and called RelNode.accept(RexShuttle) unconditionally, which re-derives expression / aggCall types and can flip a cached nullable BIGINT to BIGINT NOT NULL — breaking unrelated shapes once the shared preprocessing pipeline runs it on the two-phase aggregate path. It now detects the target shape first (read-only visitors) and only applies the rewriting RexShuttle to nodes that actually contain it, returning the original object otherwise. Adds identity (assertSame) regression tests. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Drop TimestampSubtractRewriter — superseded by upstream MinusAdapter (opensearch-project#21978) Upstream opensearch-project#21978 added MinusAdapter, the proper named ScalarFunctionAdapter for timestamp/date subtraction (t1 - t2 -> from_unixtime(to_unixtime(t1) - to_unixtime(t2))). It also coordinates with WidthBucketAdapter by deliberately leaving the MINUS(MAX OVER(), MIN OVER()) binning shape untouched. Our pre-Substrait TimestampSubtractRewriter rewrote every MINUS(timestamp, timestamp) including that binning shape, so keeping both risked clobbering the binning path. Remove the rewriter (and its test); MinusAdapter fully covers the case. The shared preprocessForSubstrait helper is retained for the remaining rewriters. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Bind distinct_count_approx after SQL-plugin emits APPROX_COUNT_DISTINCT (opensearch-project#5525) sql#5525 changed PPLBuiltinOperators.DISTINCT_COUNT_APPROX to emit a user-defined SqlAggFunction NAMED "APPROX_COUNT_DISTINCT" (DistinctCountApproxLogicalAggFunction). isthmus binds aggregate sigs by operator identity, not name, so this custom op has no substrait sig and grouped/ungrouped `stats distinct_count_approx(x)` fails with "Unable to find binding for call APPROX_COUNT_DISTINCT". (The `dc`->COUNT(DISTINCT) path works via OpenSearchDistinctCountRule/opensearch-project#22013; the named UDAF path does not.) Re-key the PplAggregateCallRewriter case from the old name DISTINCT_COUNT_APPROX to APPROX_COUNT_DISTINCT and remap the PPL marker to stock SqlStdOperatorTable.APPROX_COUNT_DISTINCT (which ADDITIONAL_AGGREGATE_SIGS binds to DataFusion approx_distinct), guarding against the stock operator itself. Remove the now-dead DISTINCT_COUNT_APPROX alias in AggregateFunction.fromNameOrError — the enum constant is APPROX_COUNT_DISTINCT, which valueOf resolves directly (addresses review comment). Verified: CalcitePPLAggregationIT.testCountDistinctApprox + testCountDistinctApproxWithAlias pass on a force-routed analytics-engine cluster. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Don't push filters below non-deterministic projects (fixes rand() predicates) `eval r = rand() | where r > 0` failed with "Comparison performance-delegation requires (RexInputRef, RexLiteral); got RAND()". Calcite's stock FILTER_PROJECT_TRANSPOSE only guards against window functions (!containsOver()), so it pushed the filter below the rand() project, inlining RAND() into the predicate — turning a delegatable ($ref > literal) comparison into RAND() > literal on the scan, which Lucene performance-delegation cannot serialize (and which must stay on the single in-memory engine, since each backend would draw different values). Replace the stock rule with FILTER_PROJECT_TRANSPOSE_DETERMINISTIC: a FilterProjectTransposeRule configured to refuse the transpose when the Project computes any non-deterministic expression (RexUtil.isDeterministic). Keeping the Filter above the Project preserves the clean ($ref > literal) shape evaluated in memory. Verified: CalcitePPLBuiltinFunctionIT.testRand passes on a force-routed analytics-engine cluster. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Restore distinct_count_approx binding on the analytics-engine route Validation requested in PR review (sandeshkr419) found that sql#5525 alone does NOT make `stats distinct_count_approx(x)` work on the analytics-engine route, so the engine-side handling removed in the prior commit is still required. On a fresh, force-routed cluster with sql#5525 deployed, CalcitePPLAggregationIT testCountDistinctApprox + testCountDistinctApproxWithAlias fail in two stages: 1. Planner stage: OpenSearchAggregateRule.resolveViableBackendsForCall reads aggCall.getAggregation().getName(), which is still "DISTINCT_COUNT_APPROX" (the DistinctCountApproxLogicalAggFunction marker). fromNameOrError then throws "No enum constant ...AggregateFunction.DISTINCT_COUNT_APPROX". 2. Substrait stage: the PplAggregateCallRewriter case keyed only on "APPROX_COUNT_DISTINCT" never matched the marker, so the unbound op reached isthmus -> "Unable to find binding for call DISTINCT_COUNT_APPROX($1)". sql#5525 only sets the substrait-emission name to APPROX_COUNT_DISTINCT, which is a later stage than both of the above, so the marker's runtime name still reaches them. Fixes: - Restore the DISTINCT_COUNT_APPROX -> APPROX_COUNT_DISTINCT alias in AggregateFunction.fromNameOrError (covers the planner path). - Match both "DISTINCT_COUNT_APPROX" and "APPROX_COUNT_DISTINCT" in the PplAggregateCallRewriter case (covers the substrait-binding path); keep the guard that skips the already-bound stock operator. - Add a fromNameOrError alias unit test. Verified on a force-routed analytics-engine cluster (parquet-backed indices, AE-routed): both tests pass; the surrounding 4-class IT sweep is unchanged at 6 pre-existing failures (date_format/strftime/percentile/nested-field), i.e. no regressions. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Add qa-module ITs for distinct_count_approx + min/max(boolean) Engine-side end-to-end regression coverage for the two analytics-engine binding fixes in this PR, run against the production /_plugins/_ppl surface on the qa-module's analytics-routed cluster (mirrors the SQL-repo CalcitePPLAggregationIT cases that proved the fixes): - DistinctCountApproxAggregationIT: grouped / aliased / dc-shorthand / ungrouped distinct_count_approx (guards the planner enum alias + rewriter rebind). - MinMaxBooleanAggregationIT: ungrouped / grouped / mixed boolean+string min/max (guards the boolean min/max substrait overload). Each IT creates its own parquet-backed (composite/parquet + lucene secondary) index, bulk-ingests a small fixed dataset, and asserts oracle values. All 7 tests pass (4 + 3). Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix date_format %c month padding; correct stale RAND comments Two small follow-ups surfaced while verifying the analytics-engine route: - date_format/strftime %c now emits the zero-padded month (Jan -> "01"), matching the PPL reference (SQL-plugin DateTimeFormatterUtil maps %c -> "MM" in date mode), which differs from stock MySQL's no-leading-zero %c. Time mode keeps the reference's single-"0" literal. Fixes the %c portion of CalciteDateTimeFunctionIT.testDateFormat and updates the Rust token-set unit test expectation accordingly. (The test's %U/%u/%V/%v week-number padding is a separate, unrelated date-format issue left for a follow-up.) - Correct two stale comments that described the old "drop the seed" RAND behavior; the adapter now rejects RAND(seed) with a clear error (addresses review feedback). No behavior change — comment/javadoc only. Verified on a force-routed analytics-engine cluster: date_format(ts,'%c') returns "01"; the now/sha2/dc/min-max ITs remain green (no regressions). Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Drop DISTINCT_COUNT_APPROX enum alias per review Remove the DISTINCT_COUNT_APPROX -> APPROX_COUNT_DISTINCT alias in AggregateFunction.fromNameOrError (and its unit test), and key the PplAggregateCallRewriter case on "APPROX_COUNT_DISTINCT" only, per @sandeshkr419's review. The SQL plugin (sql#5525) emits the operator named APPROX_COUNT_DISTINCT, which the enum resolves directly, so the legacy-name alias is no longer needed. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Address review: bulk NDJSON content-type, dc comments, error wording Review follow-ups (sandeshkr419): - QA ITs: send _bulk as application/x-ndjson via the per-index endpoint with bare {"index": {}} action metadata, matching the existing CountFastPathIT pattern. setJsonEntity alone defaults to application/json, which the bulk endpoint rejects. - Clarify the distinct_count_approx marker naming across layers: current runtime name is APPROX_COUNT_DISTINCT (sql#5525), DISTINCT_COUNT_APPROX is the legacy/defensive spelling; OpenSearchDistinctCountRule is the primary planner rewrite and PplAggregateCallRewriter is a late Substrait-emission defensive fallback. - Document the phase-specific nullability handling: the early planner rewrite preserves the marker's nullable type (LogicalAggregate.copy re-validates row type); the late Substrait rewrite pins NOT NULL (Calcite validates against the stock op's inferred type). - SPAN overflow message now includes the operands. - Drop "yet" from the seeded-RAND error so it doesn't imply imminent support. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Empty commit to retrigger CI No code change. The prior gradle-check failed in the Jenkins-trigger step (jq parse error polling the Jenkins API, 10 retries, empty result) — a transient CI-infra glitch unrelated to this PR's changes. Re-running checks. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Fix DatetimeCoverageIT %c assertions to match zero-padded month The date_format %c code change (no-leading-zero -> zero-padded "MM") aligns the analytics-engine Rust UDF with the legacy PPL contract: DateTimeFormatterUtil.DATE_HANDLERS maps %c -> "MM" (date_format dispatches through DATE_HANDLERS), so January renders as "01". The Rust code + unit test were updated for this, but the two DatetimeCoverageIT assertions still expected the old unpadded "1" — the gradle-check / sandbox-check failures. Update both assertions to "01", matching the canonical merged ITs (ppl/DateTimeFunctionIT and sql/DateTimeFunctionIT both assert "Sat Jan 01 31st ..." for the same spec). Test-only. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Enable now()/rand(seed)/sha2/span-month on the analytics-engine route + review fixes Enables these PPL functions on the force-routed analytics-engine (DataFusion) path and addresses review feedback: - now()-family: NowFspAdapter handles now([fsp]) / current_timestamp / localtimestamp, where fsp is MySQL fractional-seconds precision (0-6). - rand([seed]): RandSeedAdapter. - sha2(input, bitLen): Sha2FunctionAdapter -> encode(digest(input, 'shaN'), 'hex'), with a backend fail-clear guard for unsupported bit lengths (TODO to move validation to the frontend once the handoff carries function metadata). - span() month bucketing: SpanAdapter. - strftime: os_strftime.rs. - date_format %c: zero-padded month (DatetimeCoverageIT updated to match the merged ppl/sql DateTimeFunctionIT expectations). - PlannerImpl: don't push filters below non-deterministic projects; rand() in a pushed-down predicate would be re-evaluated and draw a fresh value. Documented as a semantic-correctness guard. - DataFusionFragmentConvertor: shared Substrait preprocess entry point. - New QA ITs: MinMaxBooleanAggregationIT. Review fixes: removed the late defensive APPROX_COUNT_DISTINCT fallback in PplAggregateCallRewriter (fail loud instead); restored the Substrait-layer assessment TODO; clarified fsp in NowFspAdapter; tidied AggregateFunction. distinct_count_approx on the analytics-engine route is handled upstream by opensearch-project#22120 and is not part of this PR. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * [analytics-engine] Add 2-shard reduce coverage for boolean min/max Addresses review feedback to cover the multi-shard case for boolean min/max. min(flag)/max(flag) over the merge_coverage dataset's boolean field are added as golden cases in the TwoShardReduceTestCase suite (opensearch-project#21951 convention), so they run at 1 shard and 2 shards with a differential equality check plus a pinned golden, exercising the two-phase per-shard-partial then coordinator-merge reduce path. The merge_coverage flag field has 15 false and 15 true rows, so min=false and max=true regardless of shard layout. Signed-off-by: Eric Wei <mengwei.eric@gmail.com> --------- Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
Description
PPL's
distinct_count_approx(x)is parsed as aSqlUserDefinedAggFunctionnamed"APPROX_COUNT_DISTINCT", not Calcite'sSqlStdOperatorTable.APPROX_COUNT_DISTINCTstdop. Substrait dispatch keys off operator identity, so the call never reached theapprox_distinctbinding and queries failed with:This PR extends
OpenSearchDistinctCountRuleto rewrite the UDF marker to the stdop, alongside the existingCOUNT(DISTINCT x)normalization. The rewritten aggregate is wrapped in aLogicalProjectthat casts back to the original nullable type — needed becauseAggregate.typeMatchesInferredpins the new aggCall to the stdop'sBIGINT NOT NULLwhile HepPlanner pins the replacement's row type to the originalBIGINT(nullable).Semantics
distinct_count_approx(field)returns an approximate distinct-value count using DataFusion's HyperLogLog++ sketch (nativeapprox_distinct). Cardinality results may differ by ±1 at small group sizes; preferred over exactcount(distinct x)for high-cardinality columns where the HLL sketch merge is dramatically cheaper than gather-then-dedupe.Query shapes now supported
The same rewrite path also normalizes single-arg
count(distinct field)(existing behaviour, unchanged).Tests
AggregateRuleTests— one for the UDF → stdop rewrite with the cast-Project wrap, one for the already-stdop no-op path.DistinctCountApproxITover the sharedcalcsdataset covers the by-group, aliased, and global query shapes end-to-end against a parquet-backed index.PPL Calcite tests in the SQL plugin unblocked
CalcitePPLAggregationIT.testCountDistinctApproxCalcitePPLAggregationIT.testCountDistinctApproxWithAliasCalcitePPLAggregationPaginatingIT.testCountDistinctApproxCalcitePPLAggregationPaginatingIT.testCountDistinctApproxWithAliasCheck 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.