This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 8f7a65bf9 perf(transforms): avoid redundant decimal truncate modulus
(#2129)
8f7a65bf9 is described below
commit 8f7a65bf968d27c520c743e9e5d95e2786d8b0d0
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 6 19:46:22 2026 +0200
perf(transforms): avoid redundant decimal truncate modulus (#2129)
Signed-off-by: Minh Vu <[email protected]>
---
transforms.go | 3 +-
transforms_decimal_bench_test.go | 61 ++++++++++++++++++++++++++++++++++++++--
transforms_test.go | 52 ++++++++++++++++++++++++++++++++++
3 files changed, 112 insertions(+), 4 deletions(-)
diff --git a/transforms.go b/transforms.go
index df0d3c363..6c66797df 100644
--- a/transforms.go
+++ b/transforms.go
@@ -784,9 +784,8 @@ func (t TruncateTransform) Transformer(src Type) (func(any)
any, error) {
val := v.(Decimal)
unscaled := val.Val.BigInt()
- // unscaled - (((unscaled % width) + width) % width)
+ // big.Int.Mod already gives a non-negative remainder
for positive widths.
applied := (&big.Int{}).Mod(unscaled, bigWidth)
- applied.Add(applied, bigWidth).Mod(applied, bigWidth)
val.Val = decimal128.FromBigInt(unscaled.Sub(unscaled,
applied))
return val
diff --git a/transforms_decimal_bench_test.go b/transforms_decimal_bench_test.go
index cd2a5e0f8..67c61b4f7 100644
--- a/transforms_decimal_bench_test.go
+++ b/transforms_decimal_bench_test.go
@@ -18,6 +18,7 @@
package iceberg_test
import (
+ "math"
"testing"
"github.com/apache/arrow-go/v18/arrow/decimal128"
@@ -25,8 +26,10 @@ import (
)
var (
- benchmarkDecimalBucketLiteralResult iceberg.Optional[iceberg.Literal]
- benchmarkDecimalBucketResult iceberg.Optional[int32]
+ benchmarkDecimalBucketLiteralResult iceberg.Optional[iceberg.Literal]
+ benchmarkDecimalBucketResult iceberg.Optional[int32]
+ benchmarkDecimalTruncateLiteralResult iceberg.Optional[iceberg.Literal]
+ benchmarkDecimalTruncateResult any
)
func BenchmarkBucketTransformDecimal(b *testing.B) {
@@ -67,3 +70,57 @@ func BenchmarkBucketTransformDecimal(b *testing.B) {
}
})
}
+
+func BenchmarkTruncateTransformDecimal(b *testing.B) {
+ values := []iceberg.Decimal{
+ {Val: decimal128.FromI64(0), Scale: 2},
+ {Val: decimal128.FromI64(1065), Scale: 2},
+ {Val: decimal128.FromI64(-1065), Scale: 2},
+ {Val: decimal128.New(0x123456789abcdef, 0xfedcba9876543210),
Scale: 2},
+ {Val: decimal128.New(-0x123456789abcdef, 0xfedcba9876543210),
Scale: 2},
+ }
+ tests := []struct {
+ name string
+ width int
+ values []iceberg.Decimal
+ }{
+ {name: "zero", width: 50, values: values[:1]},
+ {name: "positive", width: 50, values: values[1:2]},
+ {name: "negative", width: 50, values: values[2:3]},
+ {name: "wide-positive", width: 50, values: values[3:4]},
+ {name: "wide-negative", width: 50, values: values[4:5]},
+ {name: "width-1", width: 1, values: values},
+ {name: "width-max", width: math.MaxInt32, values: values},
+ }
+ for _, tt := range tests {
+ transform := iceberg.TruncateTransform{Width: tt.width}
+ literals := make([]iceberg.Literal, len(tt.values))
+ boxedValues := make([]any, len(tt.values))
+ for i, value := range tt.values {
+ literals[i] = iceberg.DecimalLiteral(value)
+ boxedValues[i] = value
+ }
+
+ b.Run(tt.name+"/Apply", func(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := range b.N {
+ benchmarkDecimalTruncateLiteralResult =
transform.Apply(iceberg.Optional[iceberg.Literal]{
+ Valid: true,
+ Val: literals[i%len(literals)],
+ })
+ }
+ })
+ b.Run(tt.name+"/Transformer", func(b *testing.B) {
+ transformer, err :=
transform.Transformer(iceberg.DecimalTypeOf(38, 2))
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := range b.N {
+ benchmarkDecimalTruncateResult =
transformer(boxedValues[i%len(boxedValues)])
+ }
+ })
+ }
+}
diff --git a/transforms_test.go b/transforms_test.go
index b65f00244..493a53441 100644
--- a/transforms_test.go
+++ b/transforms_test.go
@@ -21,6 +21,7 @@ import (
"bytes"
"fmt"
"math"
+ "math/big"
"reflect"
"strconv"
"strings"
@@ -1162,6 +1163,57 @@ func TestTruncateTransform(t *testing.T) {
}
}
+func TestTruncateTransformDecimal(t *testing.T) {
+ t.Parallel()
+
+ values := []string{
+ "0", "1", "-1", "49", "-49", "50", "-50", "51", "-51",
+ "9223372036854775808", "-9223372036854775808",
+ "18446744073709551616", "-18446744073709551616",
+ "99999999999999999999999999999999999999",
+ "-99999999999999999999999999999999999999",
+ }
+ for _, width := range []int{1, 2, 3, 10, 50, 97, math.MaxInt32} {
+ transform := iceberg.TruncateTransform{Width: width}
+ for _, scale := range []int{0, 2, 9, 38} {
+ transformer, err :=
transform.Transformer(iceberg.DecimalTypeOf(38, scale))
+ require.NoError(t, err)
+ for _, value := range values {
+ t.Run(fmt.Sprintf("width=%d/scale=%d/value=%s",
width, scale, value), func(t *testing.T) {
+ t.Parallel()
+
+ unscaled, ok :=
new(big.Int).SetString(value, 10)
+ require.True(t, ok)
+ input := iceberg.Decimal{Val:
decimal128.FromBigInt(unscaled), Scale: scale}
+ divisor := big.NewInt(int64(width))
+ quotient := new(big.Int).Div(unscaled,
divisor)
+ expected := iceberg.Decimal{
+ Val:
decimal128.FromBigInt(quotient.Mul(quotient, divisor)),
+ Scale: scale,
+ }
+
+ assert.Equal(t, expected,
transformer(input))
+ result :=
transform.Apply(iceberg.Optional[iceberg.Literal]{
+ Val:
iceberg.DecimalLiteral(input),
+ Valid: true,
+ })
+ require.True(t, result.Valid)
+ assert.Equal(t,
iceberg.DecimalLiteral(expected), result.Val)
+ assert.Equal(t, value,
input.Val.BigInt().String())
+ })
+ }
+ }
+ }
+
+ t.Run("null", func(t *testing.T) {
+ transform := iceberg.TruncateTransform{Width: 50}
+ transformer, err :=
transform.Transformer(iceberg.DecimalTypeOf(38, 2))
+ require.NoError(t, err)
+ assert.Nil(t, transformer(nil))
+ assert.False(t,
transform.Apply(iceberg.Optional[iceberg.Literal]{Valid: false}).Valid)
+ })
+}
+
func TestTruncateTransformProjectStrictNumericBounds(t *testing.T) {
tests := []struct {
name string