feat(aggregate): add Java-compatible field aggregators - #463
feat(aggregate): add Java-compatible field aggregators#463liangjie3138 wants to merge 2 commits into
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| PAIMON_RETURN_NOT_OK(input_bitmap.Deserialize(input_bytes.data(), input_bytes.size())); | ||
| accumulator_bitmap |= input_bitmap; | ||
| return VariantType(std::shared_ptr<Bytes>(accumulator_bitmap.Serialize(/*pool=*/nullptr))); | ||
| } |
There was a problem hiding this comment.
There is an issue here: the bitmap serialization currently used in Java is not cross-language compatible. To ensure compatibility, it needs to use Roaring64NavigableMap.serializePortable() instead. In some scenarios, data written by Java cannot be deserialized by C++ or other languages, and vice versa. Should we temporarily remove bitmap64 aggregation support until Java serialization is adjusted for compatibility, to avoid cases where data written in one language cannot be read in another?
| merged_field = aggregators_[i]->Agg(accumulator, input_field); | ||
| PAIMON_ASSIGN_OR_RAISE(merged_field, | ||
| aggregators_[i]->AggResult(accumulator, input_field)); | ||
| } |
There was a problem hiding this comment.
We can still use Agg in the naming here; the only difference is that it now has a return value.
| const VariantType& input_field) { | ||
| return Agg(accumulator, input_field); | ||
| } | ||
|
|
There was a problem hiding this comment.
Could we remove the newly added AggResult / AggReversedResult functions and simply update the return values of Agg / AggReversed instead?
| if (!lhs || !rhs || lhs->GetFieldCount() != rhs->GetFieldCount() || | ||
| lhs->GetFieldCount() != type->num_fields()) { | ||
| return lhs == rhs; | ||
| } |
There was a problem hiding this comment.
Should we also add a comparison for getRowKind() here?
| FieldAggregateUtils::Equals(value, candidate, element_type)); | ||
| if (equal) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
It looks like there may be some performance regression here. Given the size of this PR, maybe we could add a TODO or note documenting the current O(n²) complexity and optimize it in a follow-up.
| } | ||
|
|
||
| Result<std::vector<std::string>> CoreOptions::FieldNestedUpdateAggNestedKey( | ||
| const std::string& field_name) const { |
There was a problem hiding this comment.
Please add tests for the newly added CoreOptions APIs to CoreOptionsTest.
| pooled_unique_ptr<Bytes> result = | ||
| Bytes::AllocateBytes(serialized.size() * sizeof(T), GetDefaultPool().get()); | ||
| if (!serialized.empty()) { | ||
| std::memcpy(result->data(), serialized.data(), result->size()); |
There was a problem hiding this comment.
Just to clarify: I noticed many places are using GetDefaultPool(). Will the data volume during merging be relatively large? In paimon-cpp, batches are merged together, and data that has not yet been converted into Arrow arrays may stay held in memory for a while. Is it appropriate for that memory to be allocated from a default pool that is separate from the user-provided memory pool?
| bool input_null = DataDefine::IsVariantNull(input_field); | ||
| if (accumulator_null || input_null) { | ||
| return accumulator_null ? input_field : FieldAggregateUtils::OwnedBinary(accumulator); | ||
| } |
There was a problem hiding this comment.
Annotation 1
[P1] Preserve binary ownership in the reversed null path
When PartialUpdateMergeFunction processes an older sequence-group record, it calls AggReversedResult(accumulator, field). The base implementation swaps the arguments and invokes AggResult(field, accumulator).
For hll_sketch, and theta_sketch, if the older field is null, the null branch returns input_field directly. This value is a non-owning std::string_view produced by the use_view=true getter. If the current accumulator was created by a previous aggregation, its buffer is owned solely by the shared_ptr<Bytes> stored in row_->fields_[i].
The subsequent row_->SetField(i, result) replaces that shared_ptr<Bytes> with the returned view, releases the underlying buffer, and leaves a dangling view in the row. Later access may cause corrupted data, deserialization failures, or a use-after-free.
A valid trigger sequence is:
seq=10, sketch=A
seq=20, sketch=B // union creates owned Bytes
seq=15, sketch=NULL // enters the reversed null path
Please ensure that either null-argument order returns an owning copy of the non-null binary value, while preserving null when both arguments are null. Also add a partial-update regression test covering a sequence group, a previously aggregated binary value, and an older record with a null field. The existing OwnedAccumulatorSurvivesNullInput test only covers the forward AggResult() path.
| name, option_name, row_type->ToString())); | ||
| } | ||
| if (std::find(fields.begin(), fields.end(), index) != fields.end()) { | ||
| return Status::Invalid( |
There was a problem hiding this comment.
I recall that GetFieldIndex() also returns -1 when it encounters duplicate fields, so this find check seems unnecessary.
| } | ||
|
|
||
| Result<bool> FieldNestedUpdateAgg::RowsEqual(const InternalRow& lhs, const InternalRow& rhs) const { | ||
| for (int32_t field = 0; field < row_type_->num_fields(); ++field) { |
There was a problem hiding this comment.
Why not use FieldAggregateUtils::Equals directly here?
| CoreOptions::FromMap({{"fields.f.count-limit", "-1"}})); | ||
| ASSERT_NOK(FieldNestedUpdateAgg::Create(NestedType(), negative_limit, "f")); | ||
| } | ||
|
|
There was a problem hiding this comment.
[P1] Please align the new aggregator tests with the Java test matrix
The current C++ tests mainly cover basic happy paths and are substantially narrower than the corresponding Java tests.
The Java behavior is primarily covered by:
- paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/FieldAggregatorTest.java
- paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/FieldAggregatorRetractNullTest.java
Important missing cases include:
- collect: distinct and non-distinct aggregation/retraction for primitive, ROW, ARRAY, and MAP elements.
- nested_update: composite nested keys, multiple sequence fields, count-limit boundary cases, all null-key strategies on both accumulator and retract input, and retraction without a nested key.
- rbm64, hll_sketch, and theta_sketch: the complete null-input matrix and the unsupported-retraction contract.
These gaps allow Java compatibility issues to go unnoticed. Please port the relevant Java cases or add equivalent parameterized tests.
Please also add end-to-end write/compaction/read tests for the new aggregators to verify the final data read from files, rather than testing only the in-memory aggregation result.
Purpose
Linked issue: close #457
Port the six practical field aggregators missing on the C++ side —
collect,merge_map,nested_update,rbm64,hll_sketch,theta_sketch.Also adds
GenericArray/GenericMap, the in-memoryInternalArray/InternalMapimplementations these aggregators need to build ARRAY and MAPresults.
Tests
18 new unit test cases in
paimon-core-test, covering each aggregator'saggregate / retract / null / type-validation paths.
Three pin behaviour that is easy to "fix" incorrectly, and each was checked by
reverting the corresponding production change and confirming it fails:
BinaryAggMergeFunctionTest.OwnedAccumulatorSurvivesNullInput— drives a realAggregateMergeFunctionso the use-after-free is exercised through theproduction path.
FieldNestedUpdateAggTest.CountLimitCountsNullElementsOfAccumulator— countlimit is measured against the raw element count, matching Java.
FieldIgnoreRetractAggTest.ReversedAggBypassesWrappedOverride— the wrapperdoes not preserve the wrapped aggregator's reversed-aggregation override, also
matching Java.
No integration tests added.
API and Format
Additive only, no signatures changed:
include/paimon/defs.h: four newOptionsconstants —NESTED_KEY,NESTED_KEY_NULL_STRATEGY,NESTED_SEQUENCE_FIELD,COUNT_LIMIT.CoreOptions: aNestedKeyNullStrategyenum plus accessors.FieldAggregator: new virtuals default to the existingAgg/AggReversed.New dependency: Apache DataSketches 5.2.0 (Apache-2.0), resolved through
resolve_dependency()like the other bundled dependencies.Documentation
None.
Generative AI tooling
Generated-by: Claude Code 2.1.220 (Claude Opus 5)