ajiteshsingh opened a new pull request, #2009:
URL: https://github.com/apache/iceberg-go/pull/2009
Closes #1876.
## Summary
- Byte-reverse INT32/INT64-backed decimal row-group statistics before
storing them as Iceberg bounds.
- Leave `FIXED_LEN_BYTE_ARRAY`-backed decimals untouched — Parquet already
stores those big-endian.
- Add row-group pruning coverage for all three physical types, plus a test
that reads statistics a real Parquet writer emitted rather than hand-written
bytes.
## Why
`TestRowGroup` stored `stats.EncodeMin()`/`EncodeMax()` straight into the
bounds maps, which are later decoded with `iceberg.LiteralFromBytes`. Parquet
plain-encodes INT32/INT64 **little-endian**; an Iceberg bound is **big-endian**
two's complement. So an unscaled `659` came back as `-1828585472`:
```
parquet stats bytes 93 02 00 00
decoded as an Iceberg bound 0x93020000 = -1828585472
```
Every row group then reports a hugely negative range, `VisitEqual` and
`VisitGreater*` return `rowsCannotMatch` for all of them, and the scan silently
yields zero rows.
Byte order is the *only* difference between the two encodings, so reversing
the stat bytes is a complete conversion rather than a workaround:
```go
lower, upper := stats.EncodeMin(), stats.EncodeMax()
if intBackedDecimal(stats.Descr()) {
slices.Reverse(lower)
slices.Reverse(upper)
}
```
This takes the issue's step 2 directly instead of landing step 1 first.
Omitting the bounds is also correct — a missing bound degrades to
`rowsMightMatch` — but it disables decimal row-group pruning entirely, and
decimals are exactly the columns people filter on. Three details make the
one-liner sufficient:
- `DecimalLiteral.UnmarshalBinary` keys off the sign bit of `data[0]` and
then does full-width two's complement, so Iceberg's minimum-length rule does
not apply and the fixed 4- or 8-byte width decodes as-is.
- `EncodeMin`/`EncodeMax` return a freshly allocated buffer (`plainEncode`
copies), so reversing in place cannot disturb the row group metadata.
- arrow-go plain-encodes little-endian on big-endian hosts too
(`encoding_utils_big_endian.go` uses an explicit `binary.LittleEndian`), so the
conversion is not host-architecture dependent — relevant given the s390x build
job.
## Tests
`TestInclusiveMetricsEvalIntBackedDecimalRowGroup` covers INT32, INT64, and
an FLBA control over a row group spanning `-6.59` to `123.45` at scale 2 — the
negative minimum exercises the sign-bit decode path. Each case asserts in both
directions, so neither a corrupted bound nor a silently dropped one passes:
| Assertions | Catch |
|---|---|
| `== min`, `== max` keep the row group | a bound decoded wrongly, or
swapped with its partner |
| `< min`, `> max` prune | a bound dropped rather than converted |
| `== min-1`, `== max+1` prune | `VisitEqual`'s own pruning branches |
`TestInclusiveMetricsEvalRealParquetDecimalRowGroup` writes a real Parquet
file with an INT32-backed `decimal(9,2)` column and prunes against the metadata
the writer produced, so the hand-written fixture cannot drift from what Parquet
actually emits. It guards its own relevance with
`require.True(intBackedDecimal(stats.Descr()))`, failing loudly rather than
silently stopping covering the bug if a future arrow-go changes physical-type
selection.
Each assertion was verified to be load-bearing by mutating the source:
removing the conversion, dropping the bounds instead, swapping lower/upper,
applying the reversal to FLBA as well, and disabling each `VisitEqual` bound
check. All are caught.
## Checks
- `go test ./table/ -count=1`
- `make test-race` — 34 packages, 0 data races
- `make test-assert`
- `golangci-lint run --timeout=10m` (v2.12.2)
- `gofmt` under Go 1.26.1
- `go build ./...`, `go vet ./...`
The one failure in the full race run, `puffin`'s
`TestReaderPreservesLZ4ChecksumError`, reproduces on unmodified `main` and is
unrelated to this change.
## Note for reviewers
The bound is decoded with the **Iceberg field's** scale, not the Parquet
logical type's. Iceberg forbids changing a decimal's scale during schema
evolution (`types.go:1182` enforces equal scale and allows only precision
widening), so these agree for tables written through Iceberg. The issue does
mention a Snowflake table whose Parquet logical *precision* (3) disagrees with
the Iceberg field (38); precision does not affect decoding, and scale matched
there. If reviewers would prefer the bound rescaled from the Parquet logical
scale defensively, I'm happy to add that.
AI Disclosure: Assisted by Claude (Opus 5).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]