Skip to content

Enable PPL fillnull on the analytics-engine route via DataFusion COALESCE - #21472

Merged
mch2 merged 4 commits into
opensearch-project:mainfrom
ahkcs:pr/fillnull-poc
May 5, 2026
Merged

Enable PPL fillnull on the analytics-engine route via DataFusion COALESCE#21472
mch2 merged 4 commits into
opensearch-project:mainfrom
ahkcs:pr/fillnull-poc

Conversation

@ahkcs

@ahkcs ahkcs commented May 4, 2026

Copy link
Copy Markdown
Contributor

Description

Enables PPL fillnull on the analytics-engine query route by wiring COALESCE (and CEIL, used inside one fillnull-with-function test) into the DataFusion backend's project-side scalar capabilities. Bundled with one analytics-engine planner fix that's a prerequisite for any nested project call to round-trip through Substrait.

This is the proof-of-concept template for the remaining ~99 Bucket-1 PPL functions: every direct DataFusion mapping is a one-line addition to DataFusionAnalyticsBackendPlugin.STANDARD_PROJECT_OPS (no Rust-side, convertor, or planner changes).

How it failed before

CalciteRelNodeVisitor.visitFillNull already lowers fillnull into a clean LogicalProject(COALESCE(field, replacement)) — verified by CalcitePPLFillnullTest. But running CalciteFillNullCommandIT against a force-routed analytics-engine cluster yielded:

IllegalStateException: No backend supports scalar function [COALESCE] among [datafusion]
  at OpenSearchProjectRule.annotateExpr(OpenSearchProjectRule.java:123)

The DataFusion backend declared filterCapabilities, scanCapabilities, and aggregateCapabilities, but projectCapabilities() defaulted to empty — so OpenSearchProjectRule rejected every COALESCE it saw. Once COALESCE was declared, the fillnull-with-function test (fillnull with ceil(num1) in num0) hit a second issue:

IllegalArgumentException: Unable to convert call ANNOTATED_PROJECT_EXPR(fp64?)

OpenSearchProjectRule.annotateExpr recurses into operands and wraps every sub-call. OpenSearchProject.stripAnnotations only unwrapped the top-level wrapper, leaving an inner AnnotatedProjectExpression(CEIL) that Substrait isthmus has no converter for.

Changes

  1. OpenSearchProject.stripAnnotations — replace single-level unwrap with a RexShuttle that recursively strips AnnotatedProjectExpression at every depth before handing the plan to the backend FragmentConvertor. Adds ProjectRuleTests#testStripAnnotationsRecursivelyUnwrapsNestedExpressions covering the nested-call shape. This is a general fix — any project containing a nested scalar call would hit it once both operators are project-capable; fillnull-with-function just happened to expose it first.
  2. DataFusionAnalyticsBackendPlugin.projectCapabilities() — declare COALESCE (PPL fillnull's lowering target) on all supported field types and formats. Mirrors the existing STANDARD_FILTER_OPS pattern.
  3. DataFusionAnalyticsBackendPlugin.STANDARD_PROJECT_OPS — add CEIL. Exercised by CalciteFillNullCommandIT.testFillNullWithFunctionOnOtherField (fillnull with ceil(num1) in num0). Same one-line shape future Bucket-1 functions will use.

No Rust-side, convertor, or Substrait-extension changes — DataFusion's native runtime executes COALESCE/CEIL directly via the default Substrait extension catalog already loaded by DataFusionPlugin.loadSubstraitExtensions.

Test results

CalciteFillNullCommandIT against tests.analytics.force_routing=true with parquet-backed indices:

Phase Result
Before (baseline) 2/13 pass — only the SQL-plugin preflight type-check tests; rest fail with the No backend supports scalar function [COALESCE] error above
After this PR 13/13 pass

CalcitePPLFillnullIT: 3/3 pass (the secondary fillnull IT covering fillnull with X in ..., fillnull using f=X, and fillnull with X all-fields).

ProjectRuleTests: all existing tests pass + new nested-strip test passes.

POC framing for future Bucket-1 functions

The PPL function audit identifies ~100 Bucket-1 functions where DataFusion has a direct native mapping. After this PR's groundwork lands, each subsequent function follows the templated commit shape demonstrated by the CEIL commit:

private static final Set<ScalarFunction> STANDARD_PROJECT_OPS = Set.of(
    ScalarFunction.COALESCE,
    ScalarFunction.CEIL,
    ScalarFunction.<NEXT_FUNCTION>   // ← one line per Bucket-1 function
);

No other code changes needed — the planner picks up the new capability via BackendCapabilityProvider, isthmus emits the right Substrait, and DataFusion executes natively.

Check List

  • New functionality includes testing.
  • All tests pass.
  • New functionality has been documented (PR description; user-facing docs N/A — fillnull syntax unchanged).
  • API changes companion pull request created — N/A.
  • Public documentation issue/PR created — N/A.

DataFusion natively supports CEIL; declaring it as a project capability
lets the analytics-engine planner route Project nodes containing CEIL
through DataFusion. Same pattern as the COALESCE addition: one entry
in STANDARD_PROJECT_OPS, no convertor changes needed (Substrait
default extensions handle the conversion).

Exercised by CalciteFillNullCommandIT.testFillNullWithFunctionOnOtherField,
which calls 'fillnull with ceil(num1) in num0' — the COALESCE
replacement is a CEIL of another field. Without CEIL declared
project-capable, the planner would fail with 'No backend supports
scalar function [CEIL] among [datafusion]' even after COALESCE is
wired.

CEIL is also one of the ~100 Bucket-1 functions in the PPL→DataFusion
audit (direct DataFusion mapping, S0). This commit is the templated
shape for any other Bucket-1 scalar — add the constant to
STANDARD_PROJECT_OPS, no other changes.

Signed-off-by: Kai Huang <ahkcs@amazon.com>
@ahkcs
ahkcs requested a review from a team as a code owner May 4, 2026 18:17
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit dc560ab)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Fix recursive AnnotatedProjectExpression stripping in OpenSearchProject

Relevant files:

  • sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/rel/OpenSearchProject.java
  • sandbox/plugins/analytics-engine/src/test/java/org/opensearch/analytics/planner/ProjectRuleTests.java

Sub-PR theme: Enable PPL fillnull via DataFusion COALESCE project capabilities and integration tests

Relevant files:

  • sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionAnalyticsBackendPlugin.java
  • sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java
  • sandbox/qa/analytics-engine-rest/src/test/resources/datasets/calcs/bulk.json
  • sandbox/qa/analytics-engine-rest/src/test/resources/datasets/calcs/mapping.json

⚡ Recommended focus areas for review

JSON Injection Risk

The executePpl method builds the JSON request body by string interpolation: "{\"query\": \"" + escapeJson(ppl) + "\"}". This relies entirely on escapeJson being correct. If escapeJson (defined in the parent class, not visible here) does not handle all special characters (e.g., backslash, control characters), malformed JSON could be sent. Consider using a proper JSON serializer instead.

private Map<String, Object> executePpl(String ppl) throws IOException {
    ensureDataProvisioned();
    Request request = new Request("POST", "/_analytics/ppl");
    request.setJsonEntity("{\"query\": \"" + escapeJson(ppl) + "\"}");
    Response response = client().performRequest(request);
    return assertOkAndParse(response, "PPL: " + ppl);
}
Static Mutable State

dataProvisioned is a static boolean field used to guard one-time dataset provisioning. In test frameworks that reuse the JVM across test classes (e.g., Gradle test daemon), this flag will never reset between test runs, potentially causing tests to run against a missing or stale dataset if the provisioning step failed silently on a prior run. Consider using a @BeforeClass/@AfterClass lifecycle or an assumeTrue guard with proper teardown.

private static boolean dataProvisioned = false;

/**
 * Lazily provision the calcs dataset on first invocation. Must be called inside a test
 * method (not {@code setUp()}) — {@link org.opensearch.test.rest.OpenSearchRestTestCase}'s
 * static {@code client()} is not initialized until after {@code @BeforeClass}, but is
 * reliably available inside test bodies. Mirrors the pattern in {@code PplClickBenchIT}.
 */
private void ensureDataProvisioned() throws IOException {
    if (dataProvisioned == false) {
        DatasetProvisioner.provision(client(), DATASET);
        dataProvisioned = true;
    }
CEIL Scope Creep

CEIL is added to STANDARD_PROJECT_OPS solely to support one fillnull-with-function test, but this makes CEIL available as a general project-side capability for all queries routed through DataFusion. If CEIL is not fully validated across all field types and edge cases in the DataFusion/Substrait pipeline, this could silently produce incorrect results or runtime errors for other queries. Consider gating CEIL behind its own tracked issue or adding a comment clarifying its validation status.

private static final Set<ScalarFunction> STANDARD_PROJECT_OPS = Set.of(ScalarFunction.COALESCE, ScalarFunction.CEIL);
Non-Annotated Nested Calls

The nestedAnnotationStripper RexShuttle only strips AnnotatedProjectExpression wrappers. Plain (non-annotated) RexCall nodes that are operands of an annotated expression are passed through super.visitCall(call), which recursively visits their operands. However, if a non-annotated expression contains a nested AnnotatedProjectExpression operand (e.g., a plain RexCall wrapping an annotated sub-call), the shuttle will correctly recurse into it. This appears safe, but the interaction between super.visitCall and the annotation-stripping logic should be verified for all possible nesting combinations.

RexShuttle nestedAnnotationStripper = new RexShuttle() {
    @Override
    public RexNode visitCall(RexCall call) {
        if (call instanceof AnnotatedProjectExpression nested) {
            return nested.getOriginal().accept(this);
        }
        return super.visitCall(call);
    }
};

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to dc560ab

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Strip nested annotations from non-annotated top-level expressions

Plain (non-annotated) expressions are passed through without applying
nestedAnnotationStripper. However, a plain top-level expression (e.g., a RexCall
that was not itself wrapped) could still contain nested AnnotatedProjectExpression
operands if the annotation logic wraps only inner calls. The stripper should also be
applied to plain expressions to ensure complete removal of nested annotations.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/rel/OpenSearchProject.java [149-152]

 } else {
-    // Plain expressions have no annotation to strip — pass through.
-    strippedExprs.add(expr);
+    // Plain expressions have no top-level annotation, but may still contain
+    // nested AnnotatedProjectExpression operands — sweep them too.
+    strippedExprs.add(expr.accept(nestedAnnotationStripper));
 }
Suggestion importance[1-10]: 7

__

Why: This is a valid edge case: a plain (non-annotated) top-level RexCall could still contain nested AnnotatedProjectExpression operands, which would not be stripped by the current code. The fix ensures complete annotation removal at all depths, which is consistent with the stated goal of removing all wrappers before Substrait conversion.

Medium
Fix undefined JSON escaping method usage

The escapeJson method is called but never defined in this class, and there is no
visible import or inherited method for it. If PPL queries contain characters like
backslashes, quotes, or newlines, this will either fail to compile or produce
malformed JSON at runtime. Use a proper JSON serialization approach or ensure the
method is available.

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java [418]

-request.setJsonEntity("{\"query\": \"" + escapeJson(ppl) + "\"}");
+request.setJsonEntity("{\"query\": \"" + ppl.replace("\\", "\\\\").replace("\"", "\\\"") + "\"}");
Suggestion importance[1-10]: 3

__

Why: The escapeJson method is likely defined in the parent class AnalyticsRestTestCase (a common pattern in OpenSearch test infrastructure), so this may not be a real issue. The suggestion assumes the method is undefined without verifying the inheritance chain, making it potentially incorrect.

Low
General
Fix non-thread-safe static flag initialization

Using a non-volatile static boolean for lazy initialization in a test class is not
thread-safe. If tests run in parallel, multiple threads could pass the
dataProvisioned == false check simultaneously and provision the dataset multiple
times, potentially causing conflicts. Use a volatile modifier or an AtomicBoolean to
ensure visibility across threads.

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java [47]

-private static boolean dataProvisioned = false;
+private static volatile boolean dataProvisioned = false;
Suggestion importance[1-10]: 3

__

Why: While the volatile suggestion is technically valid for thread safety, integration tests in OpenSearch's test framework typically run sequentially within a class, making this a low-impact improvement. The risk of parallel execution causing double-provisioning is minimal in this context.

Low

Previous suggestions

Suggestions up to commit cd8e6e2
CategorySuggestion                                                                                                                                    Impact
Possible issue
Strip nested annotations from plain top-level expressions too

Plain (non-annotated) expressions are passed through without applying
nestedAnnotationStripper. If a plain top-level expression (e.g., a RexCall that was
never wrapped) contains nested AnnotatedProjectExpression operands, those inner
wrappers will survive the strip. Apply the shuttle to all expressions, not just
annotated ones.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/rel/OpenSearchProject.java [149-152]

 } else {
-    // Plain expressions have no annotation to strip — pass through.
-    strippedExprs.add(expr);
+    // Plain expressions have no annotation to strip at the top level,
+    // but may still contain nested wrappers — sweep with the shuttle.
+    strippedExprs.add(expr.accept(nestedAnnotationStripper));
 }
Suggestion importance[1-10]: 7

__

Why: This is a valid edge case: a plain (non-annotated) top-level RexCall could still contain nested AnnotatedProjectExpression operands that would survive the strip. Applying nestedAnnotationStripper to all expressions ensures complete cleanup before Substrait conversion.

Medium
Ensure JSON string escaping is correct

The escapeJson method is called but never defined in the visible code. If it is not
defined in a parent class, PPL queries containing special characters (quotes,
backslashes, newlines) will cause malformed JSON and hard-to-diagnose test failures.
Ensure the method properly escapes at minimum " and </code> characters.

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java [418]

-request.setJsonEntity("{\"query\": \"" + escapeJson(ppl) + "\"}");
+request.setJsonEntity("{\"query\": \"" + ppl.replace("\\", "\\\\").replace("\"", "\\\"") + "\"}");
Suggestion importance[1-10]: 3

__

Why: The escapeJson method is likely defined in the parent class AnalyticsRestTestCase, so this may be a false alarm. The suggestion's improved_code is also a fragile inline replacement that doesn't handle all JSON special characters, making it worse than a proper utility method.

Low
General
Fix non-thread-safe static provisioning flag

Using a static boolean flag for lazy provisioning is not thread-safe and can lead to
the dataset being provisioned multiple times or not at all when tests run in
parallel. Consider using a static AtomicBoolean or a static volatile boolean to
ensure visibility across threads.

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java [47]

-private static boolean dataProvisioned = false;
+private static volatile boolean dataProvisioned = false;
Suggestion importance[1-10]: 4

__

Why: Using volatile is a minor but valid improvement for thread visibility of the dataProvisioned flag. However, integration tests typically run single-threaded, so the practical impact is low.

Low
Suggestions up to commit f4188dd
CategorySuggestion                                                                                                                                    Impact
Possible issue
Strip nested annotations from plain top-level expressions

Plain (non-annotated) expressions are passed through without applying
nestedAnnotationStripper. However, a plain top-level expression (e.g., a RexCall)
could still contain nested AnnotatedProjectExpression instances as operands, which
would not be stripped. The shuttle should also be applied to plain expressions.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/rel/OpenSearchProject.java [149-152]

 } else {
-    // Plain expressions have no annotation to strip — pass through.
-    strippedExprs.add(expr);
+    // Plain expressions have no top-level annotation, but may still contain
+    // nested AnnotatedProjectExpression instances in their operands.
+    strippedExprs.add(expr.accept(nestedAnnotationStripper));
 }
Suggestion importance[1-10]: 7

__

Why: This is a valid edge case: a plain (non-annotated) top-level RexNode could still contain nested AnnotatedProjectExpression instances in its operands that would not be stripped by the current code. Applying nestedAnnotationStripper to plain expressions as well would make the stripping more robust and complete.

Medium
Use safe JSON serialization for request body

The escapeJson method is called but not defined in the visible code. If it only
escapes backslashes and double quotes, it may miss other JSON special characters
(newlines, tabs, etc.), leading to malformed JSON and request failures. Consider
using a proper JSON serialization library to build the request body safely.

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java [418]

-request.setJsonEntity("{\"query\": \"" + escapeJson(ppl) + "\"}");
+request.setJsonEntity("{\"query\": " + org.opensearch.common.xcontent.XContentHelper.toXContent(ppl) + "}");
Suggestion importance[1-10]: 3

__

Why: The escapeJson method is referenced but not defined in the visible code, so its correctness is unknown. However, the improved_code uses a non-standard API call that doesn't accurately reflect a proper fix, making the suggestion unreliable. The concern is valid but the proposed solution is incorrect.

Low
General
Fix thread-safety of static provisioning flag

Using a non-volatile static boolean for dataProvisioned in a test class is not
thread-safe. If tests run in parallel, multiple threads could simultaneously pass
the if (dataProvisioned == false) check and provision the dataset multiple times,
potentially causing test failures or data corruption.

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/FillNullCommandIT.java [47]

-private static boolean dataProvisioned = false;
+private static volatile boolean dataProvisioned = false;
Suggestion importance[1-10]: 4

__

Why: Adding volatile to dataProvisioned is a valid thread-safety improvement, though integration tests typically run single-threaded. The fix is minimal and correct.

Low
Suggestions up to commit 350b67d
CategorySuggestion                                                                                                                                    Impact
Possible issue
Strip nested annotations from plain expressions too

Plain (non-annotated) expressions are passed through without applying
nestedAnnotationStripper. If a plain top-level expression (e.g., a RexCall that was
never wrapped) still contains nested AnnotatedProjectExpression operands, those
wrappers will survive. Apply the shuttle to every expression, not just annotated
ones.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/rel/OpenSearchProject.java [149-152]

 } else {
-    // Plain expressions have no annotation to strip — pass through.
-    strippedExprs.add(expr);
+    // Plain expressions have no annotation to strip at the top level, but
+    // nested operands may still be wrapped — sweep them too.
+    strippedExprs.add(expr.accept(nestedAnnotationStripper));
 }
Suggestion importance[1-10]: 7

__

Why: Valid edge case: a plain (non-annotated) top-level RexNode could still contain nested AnnotatedProjectExpression operands that would survive stripping. The fix is minimal and consistent with the intent of the nestedAnnotationStripper shuttle.

Medium
Add missing test annotation to ensure execution

The test method is missing the @Test annotation (or the equivalent JUnit annotation
used by the project). Without it, the test will be silently skipped by the test
runner and will never actually validate the fix.

sandbox/plugins/analytics-engine/src/test/java/org/opensearch/analytics/planner/ProjectRuleTests.java [239]

+@Test
 public void testStripAnnotationsRecursivelyUnwrapsNestedExpressions() {
Suggestion importance[1-10]: 4

__

Why: The suggestion may be valid depending on the test framework used, but the other test methods in the file (e.g., testNestedScalarFunctions) also lack @Test annotations, suggesting the project may use a JUnit 3-style runner where public void test* methods are discovered automatically, making this annotation unnecessary.

Low
Suggestions up to commit 349c68f
CategorySuggestion                                                                                                                                    Impact
General
Remove unverified function from supported capabilities set

ScalarFunction.CEIL is included in STANDARD_PROJECT_OPS but the PR description only
mentions fillnull/COALESCE as the intended feature. If CEIL is not yet verified
through the full Substrait isthmus → DataFusion path, advertising it as a supported
project capability could cause runtime failures. Remove it or add a comment
confirming it has been validated end-to-end.

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionAnalyticsBackendPlugin.java [70-73]

+// COALESCE is the lowering target of PPL `fillnull`.
 private static final Set<ScalarFunction> STANDARD_PROJECT_OPS = Set.of(
-    ScalarFunction.COALESCE,
-    ScalarFunction.CEIL
+    ScalarFunction.COALESCE
 );
Suggestion importance[1-10]: 4

__

Why: The comment in the code explicitly states to "Add here only after verifying the function deserializes through Substrait isthmus," and CEIL is included in the test (PLUS+CEIL) specifically to exercise nested-call structure rather than as a verified production capability. This is a valid concern about advertising unverified capabilities.

Low
Ensure recursive unwrapping visits operands of unwrapped nodes

When getOriginal() returns another AnnotatedProjectExpression (e.g., double-wrapped
nodes), the current code calls accept(this) which will re-enter visitCall and handle
it correctly. However, if getOriginal() returns a non-RexCall node (e.g., a
RexInputRef), calling accept(this) on it won't invoke visitCall, so the shuttle's
super.visitCall won't process its operands. This is fine for leaf nodes, but you
should ensure the recursive unwrapping also visits operands of the unwrapped
original by calling super.visitCall on the unwrapped result when it is a RexCall.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/rel/OpenSearchProject.java [130-133]

 if (call instanceof AnnotatedProjectExpression annotated) {
-    return annotated.getOriginal().accept(this);
+    RexNode original = annotated.getOriginal();
+    return original.accept(this);
 }
 return super.visitCall(call);
Suggestion importance[1-10]: 2

__

Why: The improved_code is functionally identical to the existing_code — it just adds an intermediate variable original but the logic is unchanged. The suggestion's concern about non-RexCall nodes is already handled correctly by accept(this) dispatching to the appropriate visitor method, so there's no real bug here.

Low
Possible issue
Add missing test annotation to ensure test runs

The test method is missing the @Test annotation (or equivalent for the test
framework in use). Without it, the test may not be discovered and executed by the
test runner, meaning the regression guard it provides would silently never run.

sandbox/plugins/analytics-engine/src/test/java/org/opensearch/analytics/planner/ProjectRuleTests.java [239]

+@Test
 public void testStripAnnotationsRecursivelyUnwrapsNestedExpressions() {
Suggestion importance[1-10]: 4

__

Why: If the test framework requires @Test annotations (JUnit 4/5), the missing annotation would cause the test to never run. However, looking at the surrounding test methods in the file, none appear to use @Test either, suggesting this may be a JUnit 3-style test class where method naming convention (test*) is sufficient.

Low

ahkcs added 2 commits May 4, 2026 11:20
…ecursively

OpenSearchProjectRule.annotateExpr recurses into a project call's
operands, wrapping every sub-call in an AnnotatedProjectExpression so
the planner can track viable backends per sub-expression. The previous
OpenSearchProject.stripAnnotations only unwrapped the top-level wrapper
— for a project like PLUS(CEIL(value), value), the strip left an
inner Annotated(CEIL(...)) sitting inside the outer call. Substrait
isthmus has no converter for ANNOTATED_PROJECT_EXPR, so conversion
failed with 'Unable to convert call ANNOTATED_PROJECT_EXPR'.

Walk each project expression with a RexShuttle that recursively
unwraps every AnnotatedProjectExpression it encounters before handing
the plan off to the backend FragmentConvertor. The shuttle's
super.visitCall keeps the operand-traversal default; the override
intercepts wrappers and re-feeds the unwrapped result through the
shuttle so deeply-nested wrappers also get stripped.

Adds ProjectRuleTests#testStripAnnotationsRecursivelyUnwrapsNestedExpressions
covering the nested-call shape. The bug is independent of which scalar
functions any backend declares project-capable; it would fire for any
project with a nested scalar call once both the outer and inner
operators are project-capable on the same backend.

Signed-off-by: Kai Huang <ahkcs@amazon.com>
…ies for PPL fillnull

PPL fillnull lowers via CalciteRelNodeVisitor.visitFillNull into a
LogicalProject(COALESCE(field, replacement)) tree. The analytics-engine
planner's OpenSearchProjectRule rejects any project-list scalar call
unless at least one viable backend declares it project-capable, with
'No backend supports scalar function [COALESCE] among [datafusion]'.
DataFusion natively supports COALESCE, but the backend plugin had not
declared any projectCapabilities, so every fillnull query failed the
planner check before reaching Substrait conversion.

Add a STANDARD_PROJECT_OPS set mirroring the existing
STANDARD_FILTER_OPS pattern, seeded with COALESCE, and override
projectCapabilities() to fan it out across the same field-types and
formats the backend already advertises for filters. No Rust-side or
convertor changes are needed — the default Substrait extension catalog
loaded by DataFusionPlugin.loadSubstraitExtensions handles COALESCE
natively, and DataFusion's runtime executes it directly.

Lifts CalciteFillNullCommandIT on the force-routed analytics-engine
path from 2/13 passing (only the SQL-plugin preflight type-check tests)
to 12/13 passing. The remaining failure is testFillNullWithFunctionOnOtherField,
which uses a nested CEIL inside the COALESCE replacement — handled in
follow-up commits.

Signed-off-by: Kai Huang <ahkcs@amazon.com>
@ahkcs
ahkcs force-pushed the pr/fillnull-poc branch from 349c68f to 350b67d Compare May 4, 2026 18:21
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 350b67d

@mch2

mch2 commented May 4, 2026

Copy link
Copy Markdown
Member

lets pls add a test in /sandbox/qa to test this as well - we're going to have a lot of functions added quickly and we'll need those kinds of tests running on os core builds to prevent regression.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 350b67d: 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?

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit f4188dd

@ahkcs

ahkcs commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

lets pls add a test in /sandbox/qa to test this as well - we're going to have a lot of functions added quickly and we'll need those kinds of tests running on os core builds to prevent regression.

Done — added FillNullCommandIT under sandbox/qa/analytics-engine-rest in f4188dd2564. It runs all 13 fillnull surface forms through POST /_analytics/ppl against the QA test cluster, no SQL plugin needed.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit cd8e6e2

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for cd8e6e2: 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?

@expani expani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ahkcs LGTM

Self-contained integration test for PPL fillnull on the analytics-engine
route, mirroring CalciteFillNullCommandIT from opensearch-project/sql so
the analytics-engine path can be verified inside core without
cross-plugin dependencies on the SQL plugin.

Each test sends a PPL query through POST /_analytics/ppl (exposed by
the test-ppl-frontend plugin) which runs the same UnifiedQueryPlanner
→ CalciteRelNodeVisitor → Substrait → DataFusion pipeline as the SQL
plugin's force-routed analytics path. Covers all 13 fillnull surface
forms:

  - with X in fields           — single value, named fields
  - with X in f1,f2            — single value, multiple fields
  - using f1=X, f2=Y           — per-field replacement
  - using f0=f1                — replacement is a field reference
  - with ceil(num1) in num0    — replacement contains a nested call
                                 (exercises recursive AnnotatedProjectExpression strip)
  - chained fillnulls          — multiple commands
  - value=X (all fields)       — Calcite-specific syntax
  - value=X f1                 — Calcite-specific, named fields
  - value='N/A' str2           — Calcite-specific, string value
  - using int0=8589934592      — BIGINT into INTEGER (numeric coercion)
  - mixed-type errors          — preflight type-incompatibility validation

Provisions the 'calcs' dataset (parquet-backed) once per class via
DatasetProvisioner (which uses refresh=true, sidestepping the
LuceneCommitter.getSafeCommitInfo TODO that hangs refresh=wait_for in
the SQL plugin's IT path).

Result: 13/13 pass against the QA-module's testcluster — no SQL plugin,
no force_routing setting, no parquet-IT system properties needed.

Signed-off-by: Kai Huang <ahkcs@amazon.com>
@ahkcs
ahkcs force-pushed the pr/fillnull-poc branch from cd8e6e2 to dc560ab Compare May 4, 2026 23:33
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit dc560ab

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for dc560ab: 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?

@mch2 mch2 added skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis. skip-diff-reviewer Maintainer to skip code-diff-reviewer check, after reviewing issues in AI analysis. labels May 4, 2026
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

✅ Gradle check result for dc560ab: SUCCESS

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.42%. Comparing base (fca9ef3) to head (dc560ab).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##               main   #21472   +/-   ##
=========================================
  Coverage     73.41%   73.42%           
- Complexity    74420    74440   +20     
=========================================
  Files          5967     5967           
  Lines        338227   338230    +3     
  Branches      48754    48755    +1     
=========================================
+ Hits         248315   248346   +31     
+ Misses        70105    70061   -44     
- Partials      19807    19823   +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

RyanL1997 added a commit to RyanL1997/OpenSearch that referenced this pull request May 7, 2026
…/regexp_replace() functions

Onboards the PPL `replace` command and the `replace()` / `regexp_replace()`
eval functions to the analytics-engine route by mapping their two Calcite
lowering targets — `SqlStdOperatorTable.REPLACE` and
`SqlLibraryOperators.REGEXP_REPLACE_3` — through Substrait to DataFusion's
native `replace` and `regexp_replace` UDFs.

Same templated shape as the `fillnull` POC (opensearch-project#21472):

  ScalarFunction enum constant
    + STANDARD_PROJECT_OPS membership
    + opensearch_scalar_functions.yaml extension entry
    + ADDITIONAL_SCALAR_SIGS Calcite-op→Substrait-name bridge
    = onboarded to the analytics route.

Two scalar functions added: REPLACE (literal substring replace) and
REGEXP_REPLACE (regex replace). Both project-side only; the comparison
result of a replaced field is filtered via the existing EQUALS capability,
so no STANDARD_FILTER_OPS additions are needed.

PPL's wildcard `replace` form lowers via `WildcardUtils.convertWildcardPatternToRegex()`
to a Java-`Pattern`-compatible regex. Two flavors of Java syntax need
translation before substrait serialization, because DataFusion uses Rust's
`regex` crate which has different parsing rules:

  * `\Q…\E` quoted-literal blocks — Rust rejects `\Q` as an unrecognized
    escape sequence. The adapter expands each block to per-character
    escaped literals (semantics-preserving).
  * `$N` numeric backreferences in the replacement — Rust's replacement
    parser is identifier-greedy, so `$1_$2` is parsed as a reference to
    group named `1_` followed by `$2` (Java parses it as group 1 + literal
    underscore + group 2). The adapter wraps every numeric backreference
    in braces (`${N}`) for unambiguous Rust parsing.

Both transforms are in `RegexpReplaceAdapter` and registered against
`ScalarFunction.REGEXP_REPLACE` in `scalarFunctionAdapters()`. Calls
without `\Q` in the pattern AND without bare `$N` in the replacement pass
through unchanged.

  * `RegexpReplaceAdapterTests` — 19/19 (unquote: 9, brace: 7, dual-rewrite
    integration: 3).
  * `ReplaceCommandIT` (new self-contained QA IT, calcs dataset) — 10/10.
    Covers literal command (single + multi-pair = nested REPLACE), wildcard
    command (prefix + suffix), `replace()` and `regexp_replace()` in eval,
    full-row content checks, no-match passthrough, multi-field IN clause.
  * SQL plugin's `CalciteReplaceCommandIT` force-routed through the
    analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`
    — 21/21 in both the direct suite and the `CalciteNoPushdownIT` re-run.
    (Companion SQL plugin PR opensearch-project#5415 makes 4 column-order assertions and 1
    error-message assertion order-agnostic, mirroring the rename precedent
    from opensearch-project#5413.)

Unlike `fillnull`/`regex` where the bridge was a single one-line capability
addition, `replace`'s wildcard form exposes Java↔Rust regex syntax
divergence. The adapter is reusable for any future Calcite operator whose
PPL lowering goes through `WildcardUtils` (e.g. potential future patterns
in `like`-with-escape, custom regex lowerings).

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
mch2 pushed a commit that referenced this pull request May 7, 2026
…/regexp_replace() functions (#21527)

Onboards the PPL `replace` command and the `replace()` / `regexp_replace()`
eval functions to the analytics-engine route by mapping their two Calcite
lowering targets — `SqlStdOperatorTable.REPLACE` and
`SqlLibraryOperators.REGEXP_REPLACE_3` — through Substrait to DataFusion's
native `replace` and `regexp_replace` UDFs.

Same templated shape as the `fillnull` POC (#21472):

  ScalarFunction enum constant
    + STANDARD_PROJECT_OPS membership
    + opensearch_scalar_functions.yaml extension entry
    + ADDITIONAL_SCALAR_SIGS Calcite-op→Substrait-name bridge
    = onboarded to the analytics route.

Two scalar functions added: REPLACE (literal substring replace) and
REGEXP_REPLACE (regex replace). Both project-side only; the comparison
result of a replaced field is filtered via the existing EQUALS capability,
so no STANDARD_FILTER_OPS additions are needed.

PPL's wildcard `replace` form lowers via `WildcardUtils.convertWildcardPatternToRegex()`
to a Java-`Pattern`-compatible regex. Two flavors of Java syntax need
translation before substrait serialization, because DataFusion uses Rust's
`regex` crate which has different parsing rules:

  * `\Q…\E` quoted-literal blocks — Rust rejects `\Q` as an unrecognized
    escape sequence. The adapter expands each block to per-character
    escaped literals (semantics-preserving).
  * `$N` numeric backreferences in the replacement — Rust's replacement
    parser is identifier-greedy, so `$1_$2` is parsed as a reference to
    group named `1_` followed by `$2` (Java parses it as group 1 + literal
    underscore + group 2). The adapter wraps every numeric backreference
    in braces (`${N}`) for unambiguous Rust parsing.

Both transforms are in `RegexpReplaceAdapter` and registered against
`ScalarFunction.REGEXP_REPLACE` in `scalarFunctionAdapters()`. Calls
without `\Q` in the pattern AND without bare `$N` in the replacement pass
through unchanged.

  * `RegexpReplaceAdapterTests` — 19/19 (unquote: 9, brace: 7, dual-rewrite
    integration: 3).
  * `ReplaceCommandIT` (new self-contained QA IT, calcs dataset) — 10/10.
    Covers literal command (single + multi-pair = nested REPLACE), wildcard
    command (prefix + suffix), `replace()` and `regexp_replace()` in eval,
    full-row content checks, no-match passthrough, multi-field IN clause.
  * SQL plugin's `CalciteReplaceCommandIT` force-routed through the
    analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`
    — 21/21 in both the direct suite and the `CalciteNoPushdownIT` re-run.
    (Companion SQL plugin PR #5415 makes 4 column-order assertions and 1
    error-message assertion order-agnostic, mirroring the rename precedent
    from #5413.)

Unlike `fillnull`/`regex` where the bridge was a single one-line capability
addition, `replace`'s wildcard form exposes Java↔Rust regex syntax
divergence. The adapter is reusable for any future Calcite operator whose
PPL lowering goes through `WildcardUtils` (e.g. potential future patterns
in `like`-with-escape, custom regex lowerings).

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
imRishN pushed a commit to imRishN/OpenSearch that referenced this pull request May 8, 2026
…ESCE (opensearch-project#21472)

* [Analytics Backend / DataFusion] Wire CEIL into project capabilities

DataFusion natively supports CEIL; declaring it as a project capability
lets the analytics-engine planner route Project nodes containing CEIL
through DataFusion. Same pattern as the COALESCE addition: one entry
in STANDARD_PROJECT_OPS, no convertor changes needed (Substrait
default extensions handle the conversion).

Exercised by CalciteFillNullCommandIT.testFillNullWithFunctionOnOtherField,
which calls 'fillnull with ceil(num1) in num0' — the COALESCE
replacement is a CEIL of another field. Without CEIL declared
project-capable, the planner would fail with 'No backend supports
scalar function [CEIL] among [datafusion]' even after COALESCE is
wired.

CEIL is also one of the ~100 Bucket-1 functions in the PPL→DataFusion
audit (direct DataFusion mapping, S0). This commit is the templated
shape for any other Bucket-1 scalar — add the constant to
STANDARD_PROJECT_OPS, no other changes.

Signed-off-by: Kai Huang <ahkcs@amazon.com>

* [Analytics Engine] Strip nested AnnotatedProjectExpression wrappers recursively

OpenSearchProjectRule.annotateExpr recurses into a project call's
operands, wrapping every sub-call in an AnnotatedProjectExpression so
the planner can track viable backends per sub-expression. The previous
OpenSearchProject.stripAnnotations only unwrapped the top-level wrapper
— for a project like PLUS(CEIL(value), value), the strip left an
inner Annotated(CEIL(...)) sitting inside the outer call. Substrait
isthmus has no converter for ANNOTATED_PROJECT_EXPR, so conversion
failed with 'Unable to convert call ANNOTATED_PROJECT_EXPR'.

Walk each project expression with a RexShuttle that recursively
unwraps every AnnotatedProjectExpression it encounters before handing
the plan off to the backend FragmentConvertor. The shuttle's
super.visitCall keeps the operand-traversal default; the override
intercepts wrappers and re-feeds the unwrapped result through the
shuttle so deeply-nested wrappers also get stripped.

Adds ProjectRuleTests#testStripAnnotationsRecursivelyUnwrapsNestedExpressions
covering the nested-call shape. The bug is independent of which scalar
functions any backend declares project-capable; it would fire for any
project with a nested scalar call once both the outer and inner
operators are project-capable on the same backend.

Signed-off-by: Kai Huang <ahkcs@amazon.com>

* [Analytics Backend / DataFusion] Wire COALESCE into project capabilities for PPL fillnull

PPL fillnull lowers via CalciteRelNodeVisitor.visitFillNull into a
LogicalProject(COALESCE(field, replacement)) tree. The analytics-engine
planner's OpenSearchProjectRule rejects any project-list scalar call
unless at least one viable backend declares it project-capable, with
'No backend supports scalar function [COALESCE] among [datafusion]'.
DataFusion natively supports COALESCE, but the backend plugin had not
declared any projectCapabilities, so every fillnull query failed the
planner check before reaching Substrait conversion.

Add a STANDARD_PROJECT_OPS set mirroring the existing
STANDARD_FILTER_OPS pattern, seeded with COALESCE, and override
projectCapabilities() to fan it out across the same field-types and
formats the backend already advertises for filters. No Rust-side or
convertor changes are needed — the default Substrait extension catalog
loaded by DataFusionPlugin.loadSubstraitExtensions handles COALESCE
natively, and DataFusion's runtime executes it directly.

Lifts CalciteFillNullCommandIT on the force-routed analytics-engine
path from 2/13 passing (only the SQL-plugin preflight type-check tests)
to 12/13 passing. The remaining failure is testFillNullWithFunctionOnOtherField,
which uses a nested CEIL inside the COALESCE replacement — handled in
follow-up commits.

Signed-off-by: Kai Huang <ahkcs@amazon.com>

* [QA] Add FillNullCommandIT for the analytics-engine REST path

Self-contained integration test for PPL fillnull on the analytics-engine
route, mirroring CalciteFillNullCommandIT from opensearch-project/sql so
the analytics-engine path can be verified inside core without
cross-plugin dependencies on the SQL plugin.

Each test sends a PPL query through POST /_analytics/ppl (exposed by
the test-ppl-frontend plugin) which runs the same UnifiedQueryPlanner
→ CalciteRelNodeVisitor → Substrait → DataFusion pipeline as the SQL
plugin's force-routed analytics path. Covers all 13 fillnull surface
forms:

  - with X in fields           — single value, named fields
  - with X in f1,f2            — single value, multiple fields
  - using f1=X, f2=Y           — per-field replacement
  - using f0=f1                — replacement is a field reference
  - with ceil(num1) in num0    — replacement contains a nested call
                                 (exercises recursive AnnotatedProjectExpression strip)
  - chained fillnulls          — multiple commands
  - value=X (all fields)       — Calcite-specific syntax
  - value=X f1                 — Calcite-specific, named fields
  - value='N/A' str2           — Calcite-specific, string value
  - using int0=8589934592      — BIGINT into INTEGER (numeric coercion)
  - mixed-type errors          — preflight type-incompatibility validation

Provisions the 'calcs' dataset (parquet-backed) once per class via
DatasetProvisioner (which uses refresh=true, sidestepping the
LuceneCommitter.getSafeCommitInfo TODO that hangs refresh=wait_for in
the SQL plugin's IT path).

Result: 13/13 pass against the QA-module's testcluster — no SQL plugin,
no force_routing setting, no parquet-IT system properties needed.

Signed-off-by: Kai Huang <ahkcs@amazon.com>

---------

Signed-off-by: Kai Huang <ahkcs@amazon.com>
imRishN pushed a commit to imRishN/OpenSearch that referenced this pull request May 8, 2026
…/regexp_replace() functions (opensearch-project#21527)

Onboards the PPL `replace` command and the `replace()` / `regexp_replace()`
eval functions to the analytics-engine route by mapping their two Calcite
lowering targets — `SqlStdOperatorTable.REPLACE` and
`SqlLibraryOperators.REGEXP_REPLACE_3` — through Substrait to DataFusion's
native `replace` and `regexp_replace` UDFs.

Same templated shape as the `fillnull` POC (opensearch-project#21472):

  ScalarFunction enum constant
    + STANDARD_PROJECT_OPS membership
    + opensearch_scalar_functions.yaml extension entry
    + ADDITIONAL_SCALAR_SIGS Calcite-op→Substrait-name bridge
    = onboarded to the analytics route.

Two scalar functions added: REPLACE (literal substring replace) and
REGEXP_REPLACE (regex replace). Both project-side only; the comparison
result of a replaced field is filtered via the existing EQUALS capability,
so no STANDARD_FILTER_OPS additions are needed.

PPL's wildcard `replace` form lowers via `WildcardUtils.convertWildcardPatternToRegex()`
to a Java-`Pattern`-compatible regex. Two flavors of Java syntax need
translation before substrait serialization, because DataFusion uses Rust's
`regex` crate which has different parsing rules:

  * `\Q…\E` quoted-literal blocks — Rust rejects `\Q` as an unrecognized
    escape sequence. The adapter expands each block to per-character
    escaped literals (semantics-preserving).
  * `$N` numeric backreferences in the replacement — Rust's replacement
    parser is identifier-greedy, so `$1_$2` is parsed as a reference to
    group named `1_` followed by `$2` (Java parses it as group 1 + literal
    underscore + group 2). The adapter wraps every numeric backreference
    in braces (`${N}`) for unambiguous Rust parsing.

Both transforms are in `RegexpReplaceAdapter` and registered against
`ScalarFunction.REGEXP_REPLACE` in `scalarFunctionAdapters()`. Calls
without `\Q` in the pattern AND without bare `$N` in the replacement pass
through unchanged.

  * `RegexpReplaceAdapterTests` — 19/19 (unquote: 9, brace: 7, dual-rewrite
    integration: 3).
  * `ReplaceCommandIT` (new self-contained QA IT, calcs dataset) — 10/10.
    Covers literal command (single + multi-pair = nested REPLACE), wildcard
    command (prefix + suffix), `replace()` and `regexp_replace()` in eval,
    full-row content checks, no-match passthrough, multi-field IN clause.
  * SQL plugin's `CalciteReplaceCommandIT` force-routed through the
    analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`
    — 21/21 in both the direct suite and the `CalciteNoPushdownIT` re-run.
    (Companion SQL plugin PR opensearch-project#5415 makes 4 column-order assertions and 1
    error-message assertion order-agnostic, mirroring the rename precedent
    from opensearch-project#5413.)

Unlike `fillnull`/`regex` where the bridge was a single one-line capability
addition, `replace`'s wildcard form exposes Java↔Rust regex syntax
divergence. The adapter is reusable for any future Calcite operator whose
PPL lowering goes through `WildcardUtils` (e.g. potential future patterns
in `like`-with-escape, custom regex lowerings).

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Bukhtawar pushed a commit to Bukhtawar/OpenSearch that referenced this pull request May 10, 2026
…/regexp_replace() functions (opensearch-project#21527)

Onboards the PPL `replace` command and the `replace()` / `regexp_replace()`
eval functions to the analytics-engine route by mapping their two Calcite
lowering targets — `SqlStdOperatorTable.REPLACE` and
`SqlLibraryOperators.REGEXP_REPLACE_3` — through Substrait to DataFusion's
native `replace` and `regexp_replace` UDFs.

Same templated shape as the `fillnull` POC (opensearch-project#21472):

  ScalarFunction enum constant
    + STANDARD_PROJECT_OPS membership
    + opensearch_scalar_functions.yaml extension entry
    + ADDITIONAL_SCALAR_SIGS Calcite-op→Substrait-name bridge
    = onboarded to the analytics route.

Two scalar functions added: REPLACE (literal substring replace) and
REGEXP_REPLACE (regex replace). Both project-side only; the comparison
result of a replaced field is filtered via the existing EQUALS capability,
so no STANDARD_FILTER_OPS additions are needed.

PPL's wildcard `replace` form lowers via `WildcardUtils.convertWildcardPatternToRegex()`
to a Java-`Pattern`-compatible regex. Two flavors of Java syntax need
translation before substrait serialization, because DataFusion uses Rust's
`regex` crate which has different parsing rules:

  * `\Q…\E` quoted-literal blocks — Rust rejects `\Q` as an unrecognized
    escape sequence. The adapter expands each block to per-character
    escaped literals (semantics-preserving).
  * `$N` numeric backreferences in the replacement — Rust's replacement
    parser is identifier-greedy, so `$1_$2` is parsed as a reference to
    group named `1_` followed by `$2` (Java parses it as group 1 + literal
    underscore + group 2). The adapter wraps every numeric backreference
    in braces (`${N}`) for unambiguous Rust parsing.

Both transforms are in `RegexpReplaceAdapter` and registered against
`ScalarFunction.REGEXP_REPLACE` in `scalarFunctionAdapters()`. Calls
without `\Q` in the pattern AND without bare `$N` in the replacement pass
through unchanged.

  * `RegexpReplaceAdapterTests` — 19/19 (unquote: 9, brace: 7, dual-rewrite
    integration: 3).
  * `ReplaceCommandIT` (new self-contained QA IT, calcs dataset) — 10/10.
    Covers literal command (single + multi-pair = nested REPLACE), wildcard
    command (prefix + suffix), `replace()` and `regexp_replace()` in eval,
    full-row content checks, no-match passthrough, multi-field IN clause.
  * SQL plugin's `CalciteReplaceCommandIT` force-routed through the
    analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`
    — 21/21 in both the direct suite and the `CalciteNoPushdownIT` re-run.
    (Companion SQL plugin PR opensearch-project#5415 makes 4 column-order assertions and 1
    error-message assertion order-agnostic, mirroring the rename precedent
    from opensearch-project#5413.)

Unlike `fillnull`/`regex` where the bridge was a single one-line capability
addition, `replace`'s wildcard form exposes Java↔Rust regex syntax
divergence. The adapter is reusable for any future Calcite operator whose
PPL lowering goes through `WildcardUtils` (e.g. potential future patterns
in `like`-with-escape, custom regex lowerings).

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis. skip-diff-reviewer Maintainer to skip code-diff-reviewer check, after reviewing issues in AI analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants