Skip to content

fix(core): preserve extended aggregate sorts and options - #1264

Open
bvolpato wants to merge 1 commit into
substrait-io:mainfrom
bvolpato:bvolpato/fix-extended-aggregate-semantics
Open

bvolpato wants to merge 1 commit into
substrait-io:mainfrom
bvolpato:bvolpato/fix-extended-aggregate-semantics

Conversation

@bvolpato

@bvolpato bvolpato commented Sep 3, 2026

Copy link
Copy Markdown
Member

Extended-expression serialization drops aggregate sort fields and function options. An ordered string_agg therefore loses its ordering, and a SUM with overflow preferences loses the selected behavior. Functions referenced only by sort expressions also disappear from the extension declarations.

The loss occurs across the normal conversion path:

ExtendedExpression restored = new ProtoExtendedExpressionConverter().from(
    new ExtendedExpressionProtoConverter().toProto(expression));

Serialize the sort expressions and directions using the same expression converter and extension collector as the arguments, and preserve function-option preference order. This brings extended aggregates into line with aggregate-relation serialization.

Partially addresses #1093 for aggregate sorts and options.

@bvolpato
bvolpato force-pushed the bvolpato/fix-extended-aggregate-semantics branch from 646b87e to 1cb4864 Compare September 4, 2026 16:25
Serialize sort expressions through the shared extension collector and
retain sort directions and function-option preference order.
@bvolpato
bvolpato force-pushed the bvolpato/fix-extended-aggregate-semantics branch from 1cb4864 to 7238143 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: e1522170-5899-4d38-b540-07f10e921f62

📥 Commits

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

📒 Files selected for processing (2)
  • core/src/main/java/io/substrait/relation/AggregateFunctionProtoConverter.java
  • core/src/test/java/io/substrait/extendedexpression/ExtendedExpressionRoundTripTest.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.

Thanks, the fix is correct and both new tests fail without it. Before merging, please make RelProtoConverter.toProto(Aggregate.Measure) delegate to AggregateFunctionProtoConverter instead of copying its lines, as #1093 proposes, so the next field added to AggregateFunction can't land in only one of the two writers again.

args.get(i)
.accept(aggFuncDef, i, argVisitor, EmptyVisitationContext.INSTANCE))
.collect(Collectors.toList()))
.addAllSorts(

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.

Add a constructor that takes the caller's ExpressionProtoConverter and ExtensionCollector, then have RelProtoConverter wrap the result with the filter (AggregateRel.Measure.newBuilder().setMeasure(aggFnConverter.toProto(measure))). This also lets ExtendedExpressionProtoConverter pass the expression converter it already has, instead of building a new converter per measure.

Comment on lines +60 to +68
.addAllSorts(
measure.getFunction().sort().stream()
.map(
sort ->
SortField.newBuilder()
.setExpr(exprProtoConverter.toProto(sort.expr()))
.setDirection(sort.direction().toProto())
.build())
.collect(Collectors.toList()))

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.

Once #1342 lands, delegate to its ExpressionProtoConverter.toProto(SortField). That PR makes direction() an Optional, so this line stops compiling, and a comparison-function sort has no direction to set anyway. Drop the io.substrait.proto.SortField import too.

Suggested change
.addAllSorts(
measure.getFunction().sort().stream()
.map(
sort ->
SortField.newBuilder()
.setExpr(exprProtoConverter.toProto(sort.expr()))
.setDirection(sort.direction().toProto())
.build())
.collect(Collectors.toList()))
.addAllSorts(
measure.getFunction().sort().stream()
.map(exprProtoConverter::toProto)
.collect(Collectors.toList()))

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