fix: DH-23289: Propagate shifts properly in group-by aggregations, including rollups - #8324
fix: DH-23289: Propagate shifts properly in group-by aggregations, including rollups#8324lbooker42 wants to merge 6 commits into
Conversation
…aggregation Since deephaven#8099 the rollup re-aggregation for AggUnique reads each constituent's value column instead of its SSM, but passed that column raw. For an Instant aggregation the child level exposes its result as a LongAsInstantColumnSource, while the operator selected for Instant is LongRollupUniqueOperator, which casts the chunk to LongChunk -- so building any rollup with AggUnique over an Instant column threw ClassCastException. Reinterpret the input as long, mirroring what addBasicOperators already does at the base level, while still passing the raw type to makeUniqueOperator so it keeps selecting the Instant operator and exposing an Instant result. The reinterpret is deliberately Instant-only rather than the general ReinterpretUtils.maybeConvertToPrimitive: the latter also converts Boolean and ZonedDateTime, which route to ObjectRollupUniqueOperator and would then receive the wrong chunk type. testRollupUniqueValueTypes pins this. Also fix InstantSsmSourceWrapper.ValueWrapper's equals/hashCode, which were class-identity based and inconsistent with the ObjectVector contract, so an Instant AggDistinct result was never equal to an equivalent ObjectVectorDirect -- including the one its own getDirect() produces. Both now delegate to the ObjectVector helpers, which is what ObjectVectorDirect uses. TestRollupTable had no Instant coverage at all, on the assumption that re-aggregation is type-independent; add directed static and incremental AggUnique tests, rollup-vs-zero-key comparisons across every aggregation that accepts an Instant column, and the value-type routing test above. The directed unique test is now parameterized so one transition sequence runs for both int and Instant. The single-key incremental tests hold their table at a fixed 10k rows and sweep the per-cycle update size up to a full replacement. Previously the int one built a 100k-row table regardless of update size, spending 32s of the class's 42s to cover only deltas up to 1% and never a multi-chunk update. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No docs changes detected for 90ebd46 |
There was a problem hiding this comment.
Pull request overview
Fixes Instant-backed AggUnique re-aggregation in rollup tables.
Changes:
- Reinterprets
Instantresult sources as primitivelongsources during unique re-aggregation. - Aligns wrapped
Instantvectors’ equality and hashing with otherObjectVectorimplementations. - Adds extensive static and incremental rollup coverage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
AggregationProcessor.java |
Uses reinterpreted long sources for Instant unique re-aggregation. |
InstantSsmSourceWrapper.java |
Corrects vector equality and hash-code behavior. |
TestRollupTable.java |
Adds Instant rollup and unique-transition tests. |
…ched union Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Note for previous commit: MeasuredBaseline is a Vector with the identical
Monotonic, no crossover; Caching the leaf array inside the cursor is worth a further 1.30×, measured
|
| final int startLeaf = firstLeaf; | ||
| final int startOffset = (int) firstOffset; | ||
|
|
||
| return new ValueIteratorOfChar() { |
There was a problem hiding this comment.
There is a wrinkle, here. Vectors allow access to negative offsets, or offsets after their end, returning the appropriate null value. The column source wrapper implementations use methods like io.deephaven.engine.primitive.value.iterator.ValueIteratorOfChar#wrapWithNulls to help with this.
There are probably bugs in get, subVector, and subVectorByPositions as well.
We might consider whether SSMs as vectors need to be correct in this way. If we aren't exposing them to user code, we can get away with violating the rules (and it seems we already have been).
There was a problem hiding this comment.
SSM is not a well behaved Vector, will throw in these cases.
AggDistinct result column hands the live SSM to formulas, so X[-1], X.subVector(...), X.get(size()) all land on SSM code. Probably should address this, will create a ticket.
| } | ||
|
|
||
| return result; | ||
| return CharVector.hashCode(this); |
There was a problem hiding this comment.
I'm a bit suspicious about the implementation for equals, now. compareTo is inheriting the default.
There was a problem hiding this comment.
Good suspicion, the equals() paths were using equality operations (==) instead of Comparsions.eq(). So Float/Double NaN matching was broken. Still have inaccuracy in equality vs ObjectVector, but these are not new and don't seem to be used currently.
|
|
||
| // A child RowSet changed in place. We never see shiftChunk, so a shift below arrives here as a modify that | ||
| // dirtied only the RowSet column; dirty value columns instead mean rows genuinely came or went below us. | ||
| if (someValueColumnsModified) { |
There was a problem hiding this comment.
The below may be interesting for context, but I think the tl;dr is that we should revert this part of the change.
I'm not sure this follows.
If any group in the level below has adds or removes, all of the output aggregated columns will be in the MCS, not just some. Additionally, the exposed row set column will be in the MCS.
So, I wonder if we should be testing for containsAll (over input agg columns and input exposed row set column) instead of containsAny and calling the boolean allInputColumnsModified. Even there, we might be over-claiming adds/removes - we can't tell from here if they were all modified because of upstream value mods + upstream shifts, or if they were all modified because of adds or removes to groups.
Maybe, then, we just want to call these:
allOutputsModified
rowSetOnlyModified
|
|
||
| private RowSetBuilderRandom stepDestinationsModified; | ||
| /** Did any destination's union of child RowSets actually change contents this step? */ | ||
| private boolean rowsetsModified = false; |
There was a problem hiding this comment.
This should be a local variable in its current usage.
| @Override | ||
| public boolean hasModifications(boolean columnsModified) { | ||
| return columnsModified || rowsetsModified; | ||
| // Deliberately excludes shifts; see GroupByChunkedOperator.hasModifications. |
There was a problem hiding this comment.
I think this change makes sense - formulas don't care if our row sets change due to shifts.
| if (someKeyHasShifts) { | ||
| // Our union's keys moved but its contents did not; only the exposed RowSet column observes that. | ||
| updateModifiedColumnSet.setAll(rowSetModifiedColumnSet); | ||
| } |
There was a problem hiding this comment.
I think this is covered by the above transform. If so, I think you don't need "someKeyHasShifts" anymore.
| @Override | ||
| public ModifiedColumnSet apply(@NotNull final ModifiedColumnSet upstreamModifiedColumnSet) { | ||
| if (rowsetsModified) { | ||
| if (someKeyHasAddsOrRemoves) { |
There was a problem hiding this comment.
We don't need this either, if my proposed change to the meaning makes sense. Then again, we never did with the way the transformer was written.
| private boolean someValueColumnsModified; | ||
| private boolean someKeyHasAddsOrRemoves; | ||
| private boolean someKeyHasShifts; |
There was a problem hiding this comment.
I think we only want to keep someKeyHasAddsOrRemoves.
| stepDestinationsModified = null; | ||
| } | ||
| // Neither classification survives a step in which no union actually changed. | ||
| someKeyHasAddsOrRemoves &= rowsetsModified; |
There was a problem hiding this comment.
I think someKeyHasShifts goes away. The open question is: do we still need rowsetsModified or can we trust someKeyHasAddsOrRemoves if it's only set by addChunk and removeChunk?
|
We'll be removing the SSM work from this PR, which is being merged under #8332. We should probably write a new Jira issue to cover the group-by work that's staying in this PR. |
…ashCode changes Reverts three commits, leaving the group RowSet shift fixes in place: 36a5a0c fix: DH-23256: Reinterpret Instant value source for rollup unique re-aggregation 838eddc fix: DH-23256: Fix SSM hashCode contract and make Vector iteration O(size) c93822d fix: DH-23256: Fix SSM equals to compare contents, not leaf layout Every file the three touched is restored byte-for-byte to its pre-36a5a0c00 state, except TestRollupTable, where the revert of 36a5a0c's Instant coverage had to be resolved by hand against the two kept commits. That file keeps INCREMENTAL_TABLE_SIZE and its use by testRollupMultiKeyIncrementalInternal (afcc3e4), and the four shift tests -- testRollupGroupWithUpstreamShift (afcc3e4) plus testRollupGroupShiftOnlyReportsRowSetColumn, testRollupFormulaWithUpstreamShift, and testRollupFormulaShiftOnlyDoesNotRecompute (d2ed361) -- while dropping the Instant fixtures, the Instant rollup-vs-zero-key tests, testRollupUniqueValueTypes, and testRollupUniqueInstantStatic. The RollupCompareNugget columns-array constructor goes back to the two-argument form, since only the Instant tests needed it.
|
This PR is frozen, these changes will be moved to a new JIRA ticket to address the specific GroupBy Rollup shift issue the outstanding code is attempting to address. |
This branch fixes three distinct defects that surface when building a rollup over an
Instantcolumn and when a rollup sits above a table that shifts, plus theSegmentedSortedMultiSet(SSM) equality/hashing bugs those paths exposed.Instant re-aggregation.
Since #8099, rollup re-aggregation for
AggUniquereads each constituent's value column rather than its SSM, but passed that source through raw. For anInstantaggregation the child level exposes aLongAsInstantColumnSourcewhile the selected operator isLongRollupUniqueOperator, which casts toLongChunk— so any rollup withAggUniqueover anInstantcolumn threwClassCastException.AggregationProcessornow reinterprets the input aslongwhile still passing the raw type tomakeUniqueOperator, so operator selection and theInstantresult type are unchanged. The reinterpret is deliberately Instant-only rather than the generalReinterpretUtils.maybeConvertToPrimitive, which would also convertBoolean/ZonedDateTimeand hand the wrong chunk type toObjectRollupUniqueOperator. Relatedly,InstantSsmSourceWrapper.ValueWrapperhad class-identityequals/hashCodeinconsistent with theObjectVectorcontract — anInstantAggDistinctresult was never equal to an equivalentObjectVectorDirect, including the one its owngetDirect()produced; both now delegate to theObjectVectorhelpers, and it gains an O(size)iteratoroverride.Shift reporting through group / re-aggregate
A shift relabels row keys without changing grouped values, so
GroupByChunkedOperatorreported nothing — but rollups above it cache an unshifted union of the childRowSets and so silently went stale.GroupByChunkedOperatornow tracks shifts separately from adds/removes/modifies and dirties only the exposedRowSetcolumn when one exists (nothing at all when it doesn't), so consumers such asFormulaMultiColumnChunkedOperatorare not forced to recompute over unchanged group vectors.GroupByReaggregateOperatormirrors this one level up: it never seesshiftChunk, so a shift below arrives as a modify, and it distinguishes "the union's contents changed" from "only its keys moved" by whether the level below dirtied its value columns, propagating either all output columns or just theRowSetcolumn accordingly.SSM equality and hashing
equalscompared leaf layout structurally against another SSM, but two SSMs can hold identical values in different layouts (leaves need not be full, node sizes need not agree), so equal contents could compare unequal;hashCodemixed insize/leafCountand usedObjects.hash, so it disagreed with theVectorhash of the same contents. All eight generated SSMs now compare element-wise against anyVectorviaXComparisons.eq(fixing-0.0/NaNhandling for float/double) and hash exactly asXVector.hashCodedoes, with the leaf walk kept inline for speed and pinned against the helper by a test. Each also gains an O(size)iteratoroverride — the inherited positional one made traversalO(size * leafCount), which matters because hashing and equality run per row per cycle when an SSM-valued column is used as an aggregation key. Both replicators were updated (fixupObjectIterator, reworkedfixupObjectCompare) so the generated Object variant stays correct.Testing
TestRollupTablehad noInstantcoverage at all; it gains static and incrementalAggUniquetests, rollup-vs-zero-key comparisons across every aggregation that accepts anInstantcolumn, a value-type routing test, and four directed shift tests (group and formula, covering both correctness under shift and the shift-only "reports only the RowSet column / does not recompute" assertions). The directed unique test is parameterized so one transition sequence runs for bothintandInstant. The single-key incremental tests now hold the table at a fixed 10k rows and sweep the per-cycle update size up to a full replacement — previously the int variant built a 100k-row table regardless of update size, spending 32s of the class's 42s while only covering deltas up to 1% and never a multi-chunk update. New per-type SSM tests cover the iterator, hash/helper agreement, and symmetric equality, plus new float/double special-value tests for-0.0andNaNdelta tracking.