wave10a: 462/209 hudi/iceberg/beam-clean/samza-clean

This commit is contained in:
russell@unturf.com 2026-03-27 17:10:09 -04:00
parent ab10b2f555
commit f7fa333977
13 changed files with 979 additions and 5 deletions

View file

@ -0,0 +1,82 @@
# iceberg-0001 — SchemaUpdate.ApplyChanges List<Integer> deletes O(F×D)
## File
`core/src/main/java/org/apache/iceberg/SchemaUpdate.java`
Lines 59, 661, 717
## Defect
```java
// Line 59 — field declaration
private final List<Integer> deletes = Lists.newArrayList(); // ArrayList
// Line 660663 — field() visitor — called ONCE PER SCHEMA FIELD
@Override
public Type field(Types.NestedField field, Type fieldResult) {
int fieldId = field.fieldId();
if (deletes.contains(fieldId)) { // O(D) scan per field visit
return null;
}
// ...
}
// Line 716718 — map() visitor
public Type map(Types.MapType map, Type kResult, Type valueResult) {
int keyId = map.fields().get(0).fieldId();
if (deletes.contains(keyId)) { // O(D) scan per map
throw new IllegalArgumentException("Cannot delete map keys: " + map);
}
```
`TypeUtil.visit(schema, new ApplyChanges(...))` walks every field in the schema tree —
O(F) calls to `field()`. Each call does `deletes.contains()` on a `List<Integer>` — O(D) per call.
Also in the outer `applyChanges()` (line 533588), the loop over `identifierFieldNames` calls
`deletes.contains()` twice per identifier field (lines 549, 556) plus a while-loop over parent
chain — in total O(I × depth × D) for the validation pass.
Total schema visitor cost: **O(F × D)** where F = schema field count, D = delete count.
## Fix
Change the backing store of `deletes` from `ArrayList` to `HashSet`. The field is only ever
used for `contains()` checks and `add()` — no index-based access — so a `HashSet<Integer>` is
a drop-in replacement.
```java
// Before:
private final List<Integer> deletes = Lists.newArrayList();
// After:
private final Set<Integer> deletes = Sets.newHashSet();
```
The `ApplyChanges` inner class also holds a `List<Integer> deletes` at line 591. Change it to
`Set<Integer>` and update the constructor parameter at line 597.
```java
// Before (line 591):
private final List<Integer> deletes;
// After:
private final Set<Integer> deletes;
```
All `.contains()` calls become O(1). No other code changes required — `add()` and `contains()`
are both valid `Set` operations.
## Complexity
| | Before | After |
|-|--------|-------|
| deletes.contains() per field | O(D) | O(1) |
| full schema traversal | O(F × D) | O(F) |
At F=1000 fields, D=100 deletes: 100 000 comparisons → 1000. **100x fewer operations.**
For Iceberg tables with wide schemas (Parquet files with 500+ columns), this is significant.
## Severity
HIGH — `applyChanges()` is called on every `updateSchema()` transaction commit and on every
`SchemaUpdate.apply()` in the table metadata path. Wide schemas with batch deletes amplify
the O(F×D) factor. Schema evolution on large analytical tables hits this on every DDL operation.