Skip to content

fix(core)!: preserve custom comparison function identity - #1267

Open
bvolpato wants to merge 1 commit into
substrait-io:mainfrom
bvolpato:bvolpato/fix-custom-comparison-identity
Open

bvolpato wants to merge 1 commit into
substrait-io:mainfrom
bvolpato:bvolpato/fix-custom-comparison-identity

Conversation

@bvolpato

@bvolpato bvolpato commented Sep 3, 2026

Copy link
Copy Markdown
Member

Custom hash and merge join comparisons keep a raw function anchor while Plan conversion regenerates all declarations. A comparison referencing equal:any_any at anchor 1 can therefore resolve to not_equal:any_any after a round trip; a comparison-only function loses its declaration entirely.

Store the resolved scalar function declaration in CustomComparison, resolve it against the input plan's lookup, and register it with the output collector. This preserves identity across anchor reassignment and works with custom extension collections.

BREAKING CHANGE: CustomComparison.of(int), getCustomFunctionReference(), and the generated customFunctionReference(int) builder method are replaced by of(ScalarFunctionVariant), getDeclaration(), and declaration(ScalarFunctionVariant). Supply the comparator declaration from your extension collection instead of a plan-local integer anchor.

@bvolpato
bvolpato force-pushed the bvolpato/fix-custom-comparison-identity branch from 1f2c71e to fcf2e41 Compare September 4, 2026 16:28
Custom hash and merge join comparisons keep a raw function anchor while Plan conversion regenerates all declarations. A comparison referencing equal:any_any at anchor 1 can therefore resolve to not_equal:any_any after a round trip; a comparison-only function loses its declaration entirely.

Store the resolved scalar function declaration in CustomComparison, resolve it against the input plan's lookup, and register it with the output collector. This preserves identity across anchor reassignment and works with custom extension collections.

BREAKING CHANGE: CustomComparison.of(int), getCustomFunctionReference(), and the generated customFunctionReference(int) builder method are replaced by of(ScalarFunctionVariant), getDeclaration(), and declaration(ScalarFunctionVariant). Supply the comparator declaration from your extension collection instead of a plan-local integer anchor.
@bvolpato
bvolpato force-pushed the bvolpato/fix-custom-comparison-identity branch from fcf2e41 to cdf5187 Compare September 29, 2026 05:19
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 41 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 46d86c92-3c3e-409f-9c8a-882741f3c852

📥 Commits

Reviewing files that changed from the base of the PR and between 4d21734 and cdf5187.

📒 Files selected for processing (5)
  • core/src/main/java/io/substrait/relation/ProtoRelConverter.java
  • core/src/main/java/io/substrait/relation/RelProtoConverter.java
  • core/src/main/java/io/substrait/relation/physical/ComparisonJoinKey.java
  • core/src/test/java/io/substrait/type/proto/CustomComparisonPlanRoundtripTest.java
  • core/src/test/java/io/substrait/type/proto/HashMergeJoinKeysTest.java

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bvolpato
bvolpato marked this pull request as ready for review September 29, 2026 05:28

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

Please extend the BREAKING CHANGE: footer to cover the read-path break too, since that's the only part of the body that reaches the release notes. Reading a plan now throws unless every custom comparison's function is declared and its URN is loaded, and earlier substrait-java releases never wrote that declaration, so their plans with custom comparisons no longer read. Something like:

BREAKING CHANGE: CustomComparison.of(int), getCustomFunctionReference(), and the generated customFunctionReference(int) builder method and withCustomFunctionReference(int) wither are replaced by of(ScalarFunctionVariant), getDeclaration(), declaration(ScalarFunctionVariant) and withDeclaration(ScalarFunctionVariant). Supply the comparator declaration from your extension collection instead of a plan-local integer anchor. Reading a plan now requires each custom comparison function to be declared in the plan's extensions, with its URN loaded in the ExtensionCollection; plans with custom comparisons written by earlier substrait-java releases lack that declaration and must have one added before they can be read.

return ImmutableComparisonJoinKey.CustomComparison.builder()
.customFunctionReference(customFunctionReference)
.build();
public static CustomComparison of(SimpleExtension.ScalarFunctionVariant declaration) {

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 CustomComparison get a @Value.Check that the declaration takes two arguments and returns boolean, as the spec requires? Right now of(add:i32_i32) or of(not:bool) round-trips without error; the check is cheap now that the declaration is held, and adding it in this already-breaking PR avoids a second break later.

* A custom comparison behavior, given by a reference to a binary function with a boolean return
* type.
*/
/** A custom comparison behavior, given by a binary scalar function with a boolean return type. */

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.

The spec only says "a binary function with a boolean return type" and doesn't restrict the function kind, so resolving it as a scalar function is substrait-java's choice. Could you say so here (and in the PR body), e.g. "substrait-java resolves it as a scalar function"?

SimpleExtension.FunctionAnchor.of(
DefaultExtensionCatalog.FUNCTIONS_COMPARISON, "equal:any_any"));

private static Stream<Arguments> joinCases() {

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.

Consider replacing this 16-case nested flatMap with a flat @CsvSource of 4–5 rows, e.g. (hash, 1, filter), (merge, 1, filter), (hash, 0, no filter), (merge, -1, no filter). Those catch the same regressions and give readable case names. The comment on line 48 also reads as wire-level coverage, but the test never serializes.

}
Plan.Builder plan =
Plan.newBuilder()
.setVersion(Version.newBuilder().setMinorNumber(102))

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.

Drop this setVersion(...) line and the Version import: nothing reads the plan version, and the hard-coded 102 will go stale on the next spec bump.

merge ? rel.getMergeJoin().getKeysList() : rel.getHashJoin().getKeysList();
assertEquals(2, keys.size());
int reference = keys.get(0).getComparison().getCustomFunctionReference();
assertEquals(reference, keys.get(1).getComparison().getCustomFunctionReference());

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.

Nit, optional: the extension-count and round-trip assertions already catch a divergent second reference, so this line can go.

Suggested change
assertEquals(reference, keys.get(1).getComparison().getCustomFunctionReference());

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.

2 participants