Skip to content

Let a row dispatch declare a runtime output dtype - #9581

Open
connortsui20 wants to merge 2 commits into
developfrom
claude/generic-output-sink-etzm2l
Open

Let a row dispatch declare a runtime output dtype#9581
connortsui20 wants to merge 2 commits into
developfrom
claude/generic-output-sink-etzm2l

Conversation

@connortsui20

@connortsui20 connortsui20 commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

Right now, a row function cannot build an output whose dtype depends on its inputs. For example, if a function can accept multiple extension types and returns the same extension type (fn T -> T), the output type needs to know about the input. Right now, the output sink has to do that as well as record how to physically store this data (which may be different from the logical data type).

This PR separates logical and physical types WRT function return. The storage dtype (physical) is what the dispatched OutputElement or OutputSink builds, and stays a property of the Rust type. The output dtype (logical) is what the function returns, and each dispatch derives it from the options and the argument dtypes.

Changes

Adds RowVisitor::with_output_dtype. A dispatch calls it to declare the dtype that the framework labels (basically just reattaches at the end) onto the column the dispatch builds.

Batch execution applies the label in finalize_output, after it derives nullability and masks the null rows.

Sinks become physical only. OutputSink loses its Options type parameter, and return_dtype becomes a static storage_dtype instead. RowVisitor also loses Options parameter.

Limitations

There are still limitations. We still do not have a runtime-deciding sink parameterization. For example, if we want a FixedSizeListSink, we need a way for the sink to be able to represent different row widths. This is fixed in #9585

The other is that for types that constrain the values of the storage type (think UUIDv7 or even certain decimal types), we do not validate the storage values are correct (which maybe is fine). Additionally, there will need to be extra logic for things that have more complex wrapping logic (decimal would be the main example), but it's not a huge blocker.

API Changes

  • OutputSink<Options> becomes OutputSink
  • return_dtype(&Options) -> VortexResult<DType> becomes storage_dtype() -> DType
  • RowVisitor<Options> becomes RowVisitor and gains with_output_dtype.

`OutputSink::return_dtype` can read the function options but not the
argument dtypes, and `with_capacity` can read neither, so a row function
cannot build an output whose exact dtype depends on its inputs.

Adds `RowVisitor::with_output_dtype`, which a dispatch calls to declare
the dtype it labels onto the column it builds. Planning validates that
the label leaves every value unchanged, and batch execution applies it
after deriving nullability and masking, so empty and all-null batches
carry the same metadata as populated ones. The label reaches every visit
method, including the deferred ones that no sink-side design could serve.

Sinks become purely physical as a result: `OutputSink` loses its
`Options` type parameter, and `return_dtype` becomes a static
`storage_dtype`. `RowVisitor` loses the same parameter, which existed
only to spell the sink bound.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20 connortsui20 added the changelog/break A breaking API change label Aug 24, 2026
The previous wording derived the restriction from value preservation,
which does not hold. An extension type can constrain its storage values
through `ExtVTable::validate_scalar_value`, and `DivisibleInt` does.

The restriction is structural. An extension array holds its storage
column as a child, so a label applies by wrapping a finished column of
any encoding. No other dtype has that form, so labelling to one would be
a cast rather than a wrap.

Also records that labelling compares dtypes and does not validate values,
which is the same trust the framework already places in
`OutputElement::element_dtype`.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@codspeed-hq

codspeed-hq Bot commented Aug 24, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 16.03%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚡ 2 improved benchmarks
✅ 1979 untouched benchmarks
⏩ 54 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation take[small_m/shuffled/primitive/nonnull/chunks=2048/indices=64] 801.4 µs 684 µs +17.16%
Simulation take[small_m/shuffled/primitive/nonnull/chunks=256/indices=64] 608.1 µs 529.3 µs +14.91%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/generic-output-sink-etzm2l (f3ca34d) with develop (e4b3421)

Open in CodSpeed

Footnotes

  1. 54 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@connortsui20
connortsui20 marked this pull request as ready for review August 24, 2026 15:55
Comment on lines +138 to +140
/// The extension dtype labelled onto the finished column, when the declared output dtype
/// differs from the storage dtype.
output_label: Option<ExtDTypeRef>,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

only supports extension types for now, in the future this can be a normal DType that we support more conversions for.

It's kind of similar to our old coercion logic, but a bit more constrained

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/break A breaking API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant