Skip to content

GH-50988: [C++][Python] Substrait: add mappings for starts_with, ends_with and match_substring - #50989

Merged
kou merged 3 commits into
apache:mainfrom
jonasdedden:gh-50988-substrait-string-match
Sep 28, 2026
Merged

kou merged 3 commits into
apache:mainfrom
jonasdedden:gh-50988-substrait-string-match

Conversation

@jonasdedden

@jonasdedden jonasdedden commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

starts_with, ends_with and match_substring have no Substrait mapping, so any expression using them fails to serialize:

import pyarrow as pa
import pyarrow.compute as pc
from pyarrow.substrait import serialize_expressions

schema = pa.schema([pa.field("cat", pa.string())])
serialize_expressions([pc.starts_with(pc.field("cat"), "al")], ["f"], schema)
# ArrowNotImplementedError: No conversion function exists to convert the
# Arrow function starts_with to a Substrait call

Comparisons, isin and arithmetic serialize fine, so this is a per-function gap. It matters for engines that ingest a PyArrow filter through Substrait, where an unmappable function becomes a hard failure rather than a fallback.

See #50988.

What changes are included in this PR?

Map the three kernels onto starts_with, ends_with and contains from Substrait's functions_string.yaml, in both directions.

The signatures do not line up. Substrait passes the pattern as a second argument, while the Arrow kernels are unary and carry it in MatchSubstringOptions. So:

  • encoding lifts MatchSubstringOptions::pattern out into a literal argument
  • decoding requires that argument to be a non-null string literal, and returns NotImplemented otherwise

Substrait's case_sensitivity option maps onto ignore_case. CASE_INSENSITIVE_ASCII has no Arrow equivalent and returns NotImplemented.

Both caveats are documented in docs/source/cpp/acero/substrait.rst.

Are these changes tested?

Yes.

Substrait.StringMatchExpressionSerialization round-trips all three functions with and without ignore_case. Substrait.StringMatchExpressionDeserialization deserializes hand-written Substrait JSON to cover the default (no option) case and the two rejection paths. test_serializing_string_match_expressions round-trips each function through pyarrow.substrait.

Are there any user-facing changes?

Yes. pc.starts_with, pc.ends_with and pc.match_substring can now be serialized to Substrait and consumed back, and Acero can consume plans using Substrait's starts_with, ends_with and contains. No existing behaviour changes: these previously raised.

…, ends_with and match_substring

The Arrow kernels starts_with, ends_with and match_substring had no Substrait
mapping, so serialize_expressions raised ArrowNotImplementedError for any
expression using them.

Map them onto the starts_with, ends_with and contains functions in Substrait's
functions_string.yaml. The signatures differ: Substrait takes the pattern as a
second argument while the Arrow kernels are unary and carry it in
MatchSubstringOptions, so the decoder requires that argument to be a non-null
string literal. Substrait's case_sensitivity option maps onto ignore_case;
CASE_INSENSITIVE_ASCII has no Arrow equivalent and returns NotImplemented.
@github-actions github-actions Bot added the awaiting review Awaiting review label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #50988 has been automatically assigned in GitHub to PR creator.

@jonasdedden

Copy link
Copy Markdown
Contributor Author

CI failures:

ARM64 macOS GLib & Ruby - red-arrow-flight crashes in TestFlightSQLClient. Already tracked as #50919. Is about Flight, no Substrait involved.

AMD64 Conda C++ AVX2 - 2 of 109 tests failed, arrow-filesystem-test and arrow-s3fs-module-test. "Invalid: Attempt to initialize S3 after it has been finalized." Already tracked as #50930. Substrait test arrow-substrait-substrait-test passed.

AMD64 macOS 15-intel C++ - 1 of 107 tests failed, arrow-gcsfs-test: "Could not start GCS emulator 'storage-testbench' (failed to listen)". The emulator did not come up on the runner. Substrait test arrow-substrait-substrait-test passed.

=> I believe the CI failures are not because of changes introduced in this PR.

@jonasdedden

Copy link
Copy Markdown
Contributor Author

AMD64 macOS 15-intel C++ - 1 of 107 tests failed, arrow-gcsfs-test: "Could not start GCS emulator 'storage-testbench' (failed to listen)". => Raised here and fixed here.

@jonasdedden

Copy link
Copy Markdown
Contributor Author

@rok or @pitrou , any reviews? :)

* Deserialize a bare Expression against an ExtensionSet instead of
  hand-writing a full ExtendedExpression
* Drop the nested invert() round trip, which only exercises generic
  call serialization
* Substrait string literals always decode to utf8, so check for that
  directly
* Drop the unreachable arity check in the encoder
* Reduce the Python test to one case per function
@jonasdedden

Copy link
Copy Markdown
Contributor Author

@kou @felipecrv could you take a look when you have a chance? This adds Substrait mappings for starts_with, ends_with and match_substring, following the same pattern as the hyperbolic trig mappings in #44630.

I trimmed the PR down a bit in 7ec5d6d.

@kou kou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1

@zanmato1984 Do you want to review this?

Comment on lines +1023 to +1024
substrait_call.SetValueArg(0, call.arguments[0]);
substrait_call.SetValueArg(1, compute::literal(match_options->pattern));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we check call.arguments[0] type and match_options->pattern type because Substrait accepts only string but the compute module accepts non-string like binary?

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.

Good catch. I think we should check the bound input type before using the standard Substrait string mapping. These Arrow kernels also accept binary and large binary/string inputs, but functions_string.yaml has no matching signatures for those types, so the current encoder can produce an invalid standard Substrait call. With the current type mappings, I think we should only accept plain string input here, return NotImplemented for unsupported types, and add a binary regression test. We should also ensure the pattern is valid for a Substrait string literal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, done. The encoder now requires utf8 input and a valid UTF-8 pattern, and returns NotImplemented otherwise. large_utf8 is rejected too, because it's serialized as a user-defined type that wouldn't match Substrait's string/varchar signatures.

@github-actions github-actions Bot added awaiting review Awaiting review awaiting merge Awaiting merge awaiting committer review Awaiting committer review and removed awaiting review Awaiting review awaiting merge Awaiting merge labels Sep 28, 2026
Substrait's starts_with / ends_with / contains only accept strings while
the Arrow kernels also accept binary-like input, and MatchSubstringOptions
may hold a pattern that is not valid UTF-8. Return NotImplemented for
those instead of producing an invalid Substrait call.

@zanmato1984 zanmato1984 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.

+1

Thanks for working on this.

@kou
kou merged commit c75bc9a into apache:main Sep 28, 2026
61 of 63 checks passed
@kou kou removed the awaiting committer review Awaiting committer review label Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants