Skip to content

feat(isthmus): convert REVERSE and INITCAP from Calcite - #1251

Merged
nielspardon merged 4 commits into
substrait-io:mainfrom
alexandrefimov:issue-1222-reverse-initcap
Sep 4, 2026
Merged

feat(isthmus): convert REVERSE and INITCAP from Calcite#1251
nielspardon merged 4 commits into
substrait-io:mainfrom
alexandrefimov:issue-1222-reverse-initcap

Conversation

@alexandrefimov

@alexandrefimov alexandrefimov commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

REVERSE and INITCAP had no FunctionMappings entry, and FunctionConverter builds its signature map from that list alone, so neither had a FunctionFinder and no call to either converted. functions_string.yaml declares both, so the mapping was the whole gap; without it a call fails with Unable to convert call REVERSE(char<16>?).

INITCAP binds Calcite's operator. REVERSE binds a local SqlFunction with ARG0_NULLABLE, registered in SUBSTRAIT_SCALAR_OPERATOR_TABLE where a lookup reaches it before the libraries, because Calcite's is wrong twice over. SqlLibraryOperators.REVERSE infers its return with ARG0_NULLABLE_VARYING, which widens a CHAR: reverse(c16) bound the fixedchar variant and then recorded varchar<16> as its output type, where the declared fixedchar<L1> makes it fixedchar<16>. And Calcite gives the Spark library's operator the same name, so with both libraries enabled the validator finds two candidates and discards both, which is why an ORDER BY over the call failed to validate. The same collision catches least and greatest, filed as #1274.

Neither fault is visible to a round trip, which carries the recorded output_type verbatim in both directions, so the two new tests assert the invocation's output type and the three ORDER BY forms directly.

Closes #1222

FunctionConverter builds its signature map from the FunctionMappings list alone,
with no fall-back matching an operator to a same-named Substrait function, so an
operator missing from that table has no finder and every call to it fails. Both
functions are declared in functions_string.yaml with character implementations
and both have a Calcite operator, so the entry joining them is all that was
missing.

The round-trip tests fail without the two entries with "Unable to convert call
REVERSE(char<16>?)" and the same for INITCAP.

Closes substrait-io#1222

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

Map reverse to an Isthmus-local operator instead of SqlLibraryOperators.REVERSE — as written it is unreachable in an ORDER BY and emits a non-conformant output type, and one change fixes both. initcap is correct as it stands.

One note on the PR body, since title + body become the squash-merge commit that semantic-release turns into CHANGELOG.md: "exactly the ones you named" has no referent in git log, and the round-trip-test paragraph is what the diff already shows. The FunctionConverter-builds-its-map-from-FunctionMappings-alone sentence is the rationale worth keeping, and Unable to convert call REVERSE(char<16>?) is worth keeping as the symptom.

Comment thread isthmus/src/main/java/io/substrait/isthmus/expression/FunctionMappings.java Outdated
Comment thread isthmus/src/test/java/io/substrait/isthmus/StringFunctionTest.java Outdated
Comment thread isthmus/src/test/java/io/substrait/isthmus/StringFunctionTest.java Outdated
SqlLibraryOperators.REVERSE was wrong on two counts. Its
ReturnTypes.ARG0_NULLABLE_VARYING widens a CHAR to VARCHAR, so reverse(c16)
emitted reverse:fchar with output_type varchar<16> where the declaration says
fixedchar<L1>, against algebra.proto's "exactly as derived using the declaration
in the extension". And Calcite names the Spark library's operator REVERSE too, so
with both libraries enabled a lookup by that name returns two candidates,
filterOperatorRoutinesByKind keeps neither, and every ORDER BY over it fails
validation with "No match found for function signature REVERSE(<CHARACTER>)".

An Isthmus-local operator with ARG0_NULLABLE fixes the return, and registering it
in SUBSTRAIT_SCALAR_OPERATOR_TABLE, which is consulted first, fixes the lookup.
This is the shape RIGHTSHIFT already uses in the same file.

A round trip cannot see either fault: both directions carry the recorded
output_type verbatim, so the widened call round-trips green. The two new tests
assert the invocation and the three ORDER BY forms directly, and all four cases
fail on the library operator.

The third column of the fixture is covered too, so initcap:str and reverse:str
are no longer the only implementations untested.

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

Verified the fix: the local operator and the SUBSTRAIT_SCALAR_OPERATOR_TABLE entry both landed as discussed, and I reproduced your negative result — reverting just the two main-source files fails all four new cases, expected: <FixedChar{nullable=true, length=16}> but was: <VarChar{nullable=true, length=16}> plus the three ORDER BY forms. :isthmus:check and :isthmus-cli:check are green on the merge with current main, which has since moved to substrait-packaging 0.102.0 (functions_string.yaml is byte-identical to 0.101.0, so the declarations the change relies on are unchanged).

One small documentation point inline. The rewritten PR body reads well and Unable to convert call REVERSE(char<16>?) is the right thing to have kept.

alexandrefimov and others added 2 commits September 4, 2026 10:48
Calcite's Enumerable convention cannot execute it: unlike the library operator it
replaces, it has no RexImpTable implementor. RIGHTSHIFT and DATETIME_SUBTRACT
replaced nothing, so their own lack of one costs nothing; this one displaces an
operator that had it, and that belongs in the Javadoc rather than in a stack
trace.

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

LGTM

@nielspardon
nielspardon merged commit fffa05f into substrait-io:main Sep 4, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isthmus: REVERSE and INITCAP have no FunctionMappings entry, so neither converts from Calcite

2 participants