Skip to content

feat: support preserving reads for state-heavy accumulators - #77

Merged
rkrishn7 merged 2 commits into
branch-55from
state-heavy-preserving-grouped-reads
Sep 10, 2026
Merged

rkrishn7 merged 2 commits into
branch-55from
state-heavy-preserving-grouped-reads

Conversation

@rkrishn7

@rkrishn7 rkrishn7 commented Sep 8, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

The preserving grouped-read APIs added in #74 are not implemented by the state-heavy grouped accumulators. A caller taking a selective snapshot therefore cannot use those APIs when a plan contains approx_distinct, array_agg, ordered first/last, median, or percentile_cont.

What changes are included in this PR?

  • Implement preserving evaluation and intermediate-state reads for the five specialized grouped accumulators.
  • Extend first/last's private value-state abstraction across primitive, byte/view, and nested values.
  • Preserve selection order, duplicate indices, typed empty outputs, and later updates.
  • Clone median and percentile inputs before their in-place selection algorithms run.
  • Check variable-width and list offsets while constructing selected output.

The existing destructive evaluate and state paths are unchanged. Preserving-only allocations and indexing are kept in the new methods.

Are these changes tested?

Yes. Unit tests cover repeated reads, reordered and duplicate selections, empty selections, intermediate-state round trips, and updates after a preserving read. I also ran:

  • cargo fmt --all
  • cargo clippy --all-targets --all-features -- -D warnings
  • The full extended workspace test command from the contributor guide
  • Aggregate tests with force_hash_collisions

The workspace run had one memory-threshold test exceed its limit by 2.4 MB; that test passed when rerun in isolation.

Are there any user-facing changes?

The existing optional preserving-read APIs now report support for these grouped accumulators. There are no public API signature changes and no SQL behavior changes.

Comment on lines +605 to +607
for index in selection.iter() {
self.groups[index].serialize(&mut scratch);
builder.append_value(&scratch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we check the accumulated bytes to avoid overflow? In other words, return an error rather than panic.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added

let array = if selection.is_empty() {
self.default_orderings[column_index].to_array_of_size(0)?
} else {
ScalarValue::iter_to_array(selection.iter().map(|group_index| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ditto.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Unlike HLL, this path uses ScalarValue::iter_to_array, which also backs destructive state() and supports nested values. A local string/binary check would be incomplete. I think it's worth punting on this for now

}
}

fn build_preserving(&self, selection: GroupSelection<'_>) -> Result<ArrayRef> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: the first_last preserving-read test only exercises PrimitiveValueState (Int64), so this BytesValueState::build_preserving (and GenericValueState::build_preserving below) are uncovered — including the unsafe from_utf8_unchecked and the i32/i64 offset-capacity math here. Worth adding a Utf8/string first/last preserving test (and ideally a List/nested one) to lock these paths.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added!


fn build_state_preserving(&self, selection: GroupSelection<'_>) -> Result<ArrayRef> {
selection.validate_num_groups(self.group_values.len())?;
let mut offsets = Vec::with_capacity(selection.len() + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit (optional): this offset-building pattern (offsets vec seeded with 0 + checked_add running total + i32::try_from capacity guard) repeats here, in array_agg.rs build_selected_groups, and in first_last/state.rs BytesValueState::build_preserving. Could factor into a small shared helper that returns the OffsetBuffer; the per-site value flattening differs, so this is low priority — feel free to leave it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for suggestion. Going to leave this unfactored as the sites differ in offset semantics and how the values are materialized

@rkrishn7
rkrishn7 merged commit 6c91685 into branch-55 Sep 10, 2026
66 checks passed
@rkrishn7
rkrishn7 deleted the state-heavy-preserving-grouped-reads branch September 10, 2026 15:40
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