zeroshade commented on code in PR #1675:
URL: https://github.com/apache/iceberg-go/pull/1675#discussion_r3926372642


##########
table/internal/partition_predicate.go:
##########
@@ -151,6 +196,110 @@ func BuildPartitionMatchPredicate(spec 
iceberg.PartitionSpec, schema *iceberg.Sc
        return result, nil
 }
 
+func partitionTerm(transform iceberg.Transform, name string) 
iceberg.UnboundTerm {
+       ref := iceberg.Reference(name)
+       if isIdentityTransform(transform) {
+               return ref
+       }
+       if isVoidTransform(transform) {
+               // Use the value form so binding the null predicate can fold it 
to

Review Comment:
   **minor** — partitionTerm's void branch is unreachable dead code with a 
misleading comment
   
   partitionTerm is called from only two sites (:141 and :155), both reached 
only when fr.isVoid is false, and fr.isVoid is set exactly when 
isVoidTransform(f.Transform) is true. The void branch at :204-208 can therefore 
never execute. Its comment ('Use the value form so binding the null predicate 
can fold it to AlwaysTrue') describes an outcome that never occurs, because 
void fields never produce a predicate at all — they are skipped at :136-142. 
Delete the branch and the comment.



##########
table/internal/partition_predicate_test.go:
##########
@@ -276,38 +290,406 @@ func 
TestBuildPartitionMatchPredicate_NoSeparatorCollision(t *testing.T) {
        assert.True(t, expr.Equals(want), "distinct tuples must not be deduped, 
got %s", expr)
 }
 
-func TestBuildPartitionMatchPredicate_NonIdentityTransformRejected(t 
*testing.T) {
+func TestBuildPartitionMatchPredicate_UsesNonIdentityTransform(t *testing.T) {
        cases := []struct {
                name      string
-               transform iceberg.Transform
+               field     iceberg.PartitionField
+               source    string
+               value     any
+               wantValue iceberg.Literal
        }{
-               {"bucket", iceberg.BucketTransform{NumBuckets: 4}},
-               {"truncate", iceberg.TruncateTransform{Width: 10}},
-               {"day", iceberg.DayTransform{}},
+               {
+                       name:      "bucket",
+                       field:     iceberg.PartitionField{SourceIDs: []int{1}, 
FieldID: 1000, Name: "id_part", Transform: iceberg.BucketTransform{NumBuckets: 
4}},
+                       source:    "id",
+                       value:     int32(1),
+                       wantValue: iceberg.Int32Literal(1),
+               },
+               {
+                       name:      "truncate",
+                       field:     iceberg.PartitionField{SourceIDs: []int{2}, 
FieldID: 1000, Name: "category_part", Transform: 
iceberg.TruncateTransform{Width: 3}},
+                       source:    "category",
+                       value:     "boo",
+                       wantValue: iceberg.StringLiteral("boo"),
+               },
        }
 
        for _, tc := range cases {
                t.Run(tc.name, func(t *testing.T) {
-                       spec := identitySpec(iceberg.PartitionField{SourceIDs: 
[]int{1}, FieldID: 1000, Name: "id_part", Transform: tc.transform})
+                       spec := specWithFields(tc.field)
 
-                       _, err := BuildPartitionMatchPredicate(spec, 
dynamicOverwriteSchema(), []map[int]any{{1000: int32(1)}})
-                       require.Error(t, err)
-                       assert.ErrorIs(t, err, iceberg.ErrNotImplemented)
+                       expr, err := BuildPartitionMatchPredicate(spec, 
dynamicOverwriteSchema(), []map[int]any{{1000: tc.value}})
+                       require.NoError(t, err)
+
+                       want := iceberg.LiteralPredicate(iceberg.OpEQ,
+                               iceberg.NewUnboundTransform(tc.field.Transform, 
iceberg.Reference(tc.source)),
+                               tc.wantValue,
+                       )
+                       assert.True(t, expr.Equals(want), "want %s, got %s", 
want, expr)
                })
        }
 }
 
+func bucketValueForInt32(t *testing.T, transform iceberg.BucketTransform, 
value int32) int32 {
+       t.Helper()
+
+       result := transform.Apply(iceberg.Optional[iceberg.Literal]{
+               Valid: true,
+               Val:   iceberg.Int32Literal(value),
+       })
+       require.True(t, result.Valid)
+
+       return result.Val.(iceberg.Int32Literal).Value()
+}
+
+func TestBuildPartitionMatchPredicate_EvaluatesTransforms(t *testing.T) {
+       bucketTransform := iceberg.BucketTransform{NumBuckets: 4}
+       bucketPartition := bucketValueForInt32(t, bucketTransform, 1)
+       bucketNonMatch := int32(2)
+       for bucketValueForInt32(t, bucketTransform, bucketNonMatch) == 
bucketPartition {
+               bucketNonMatch++
+       }
+
+       cases := []struct {
+               name            string
+               sourceType      iceberg.Type
+               transform       iceberg.Transform
+               partitionValue  any
+               matchingValues  []any
+               nonMatchingVals []any
+       }{
+               {
+                       name:            "bucket",
+                       sourceType:      iceberg.PrimitiveTypes.Int32,
+                       transform:       bucketTransform,
+                       partitionValue:  bucketPartition,
+                       matchingValues:  []any{int32(1)},
+                       nonMatchingVals: []any{bucketNonMatch},
+               },
+               {
+                       name:            "truncate",
+                       sourceType:      iceberg.PrimitiveTypes.String,
+                       transform:       iceberg.TruncateTransform{Width: 3},
+                       partitionValue:  "boo",
+                       matchingValues:  []any{"boo", "books", "booster"},
+                       nonMatchingVals: []any{"bar", "science"},
+               },
+               {
+                       name:           "year",
+                       sourceType:     iceberg.PrimitiveTypes.Timestamp,
+                       transform:      iceberg.YearTransform{},
+                       partitionValue: int32(50), // 2020 - 1970
+                       matchingValues: []any{
+                               timestampAtUTC(2020, 1, 1, 0, 0, 0, 0),
+                               timestampAtUTC(2020, 12, 31, 23, 59, 59, 
999999),
+                       },
+                       nonMatchingVals: []any{
+                               timestampAtUTC(2019, 12, 31, 23, 59, 59, 
999999),
+                               timestampAtUTC(2021, 1, 1, 0, 0, 0, 0),
+                       },
+               },
+               {
+                       name:           "month",
+                       sourceType:     iceberg.PrimitiveTypes.Timestamp,
+                       transform:      iceberg.MonthTransform{},
+                       partitionValue: int32(601), // 2020-02, relative to 
1970-01
+                       matchingValues: []any{
+                               timestampAtUTC(2020, 2, 1, 0, 0, 0, 0),
+                               timestampAtUTC(2020, 2, 29, 23, 59, 59, 999999),
+                       },
+                       nonMatchingVals: []any{
+                               timestampAtUTC(2020, 1, 31, 23, 59, 59, 999999),
+                               timestampAtUTC(2020, 3, 1, 0, 0, 0, 0),
+                       },
+               },
+               {
+                       name:           "day",
+                       sourceType:     iceberg.PrimitiveTypes.Timestamp,
+                       transform:      iceberg.DayTransform{},
+                       partitionValue: dateAtUTC(2020, 2, 29),
+                       matchingValues: []any{
+                               timestampAtUTC(2020, 2, 29, 0, 0, 0, 0),
+                               timestampAtUTC(2020, 2, 29, 23, 59, 59, 999999),
+                       },
+                       nonMatchingVals: []any{
+                               timestampAtUTC(2020, 2, 28, 23, 59, 59, 999999),
+                               timestampAtUTC(2020, 3, 1, 0, 0, 0, 0),
+                       },
+               },
+               {
+                       name:           "hour",
+                       sourceType:     iceberg.PrimitiveTypes.Timestamp,
+                       transform:      iceberg.HourTransform{},
+                       partitionValue: int32(439714), // 2020-02-29 10:00 UTC, 
relative to 1970
+                       matchingValues: []any{
+                               timestampAtUTC(2020, 2, 29, 10, 0, 0, 0),
+                               timestampAtUTC(2020, 2, 29, 10, 59, 59, 999999),
+                       },
+                       nonMatchingVals: []any{
+                               timestampAtUTC(2020, 2, 29, 9, 59, 59, 999999),
+                               timestampAtUTC(2020, 2, 29, 11, 0, 0, 0),
+                       },
+               },
+               {
+                       name:           "hour before epoch",
+                       sourceType:     iceberg.PrimitiveTypes.Timestamp,
+                       transform:      iceberg.HourTransform{},
+                       partitionValue: int32(-1), // 1969-12-31 23:00 UTC
+                       matchingValues: []any{
+                               timestampAtUTC(1969, 12, 31, 23, 0, 0, 0),
+                               timestampAtUTC(1969, 12, 31, 23, 59, 59, 
999999),
+                       },
+                       nonMatchingVals: []any{
+                               timestampAtUTC(1969, 12, 31, 22, 59, 59, 
999999),
+                               timestampAtUTC(1970, 1, 1, 0, 0, 0, 0),
+                       },
+               },
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       schema := iceberg.NewSchema(0, iceberg.NestedField{
+                               ID: 1, Name: "value", Type: tc.sourceType,
+                       })
+                       spec := specWithFields(iceberg.PartitionField{
+                               SourceIDs: []int{1}, FieldID: 1000, Name: 
"value_part", Transform: tc.transform,
+                       })
+
+                       expr, err := BuildPartitionMatchPredicate(spec, schema, 
[]map[int]any{{1000: tc.partitionValue}})
+                       require.NoError(t, err)
+
+                       eval, err := iceberg.ExpressionEvaluator(schema, expr, 
true)
+                       require.NoError(t, err)
+
+                       for _, value := range tc.matchingValues {
+                               matched, err := 
eval(partitionPredicateRow{value})
+                               require.NoError(t, err)
+                               assert.True(t, matched, "source value %v should 
match partition value %v", value, tc.partitionValue)
+                       }
+                       for _, value := range tc.nonMatchingVals {
+                               matched, err := 
eval(partitionPredicateRow{value})
+                               require.NoError(t, err)
+                               assert.False(t, matched, "source value %v 
should not match partition value %v", value, tc.partitionValue)
+                       }
+               })
+       }
+}
+
+func TestBuildPartitionMatchPredicate_EvaluatesBucketCollision(t *testing.T) {
+       transform := iceberg.BucketTransform{NumBuckets: 4}
+       valuesByBucket := make(map[int32][]int32)
+       var collisionBucket int32
+       var collisionValues []int32
+
+       for value := range int32(10000) {
+               bucket := bucketValueForInt32(t, transform, value)
+               valuesByBucket[bucket] = append(valuesByBucket[bucket], value)
+               if len(valuesByBucket[bucket]) == 2 {
+                       collisionBucket = bucket
+                       collisionValues = valuesByBucket[bucket]
+
+                       break
+               }
+       }
+       require.Len(t, collisionValues, 2)
+       assert.NotEqual(t, collisionValues[0], collisionValues[1])
+
+       nonMatchingValue := int32(-1)
+       for value := range int32(10000) {
+               if bucketValueForInt32(t, transform, value) != collisionBucket {
+                       nonMatchingValue = value
+
+                       break
+               }
+       }
+       require.NotEqual(t, int32(-1), nonMatchingValue)
+
+       schema := iceberg.NewSchema(0, iceberg.NestedField{
+               ID: 1, Name: "id", Type: iceberg.PrimitiveTypes.Int32,
+       })
+       spec := specWithFields(iceberg.PartitionField{
+               SourceIDs: []int{1}, FieldID: 1000, Name: "id_bucket", 
Transform: transform,
+       })
+       expr, err := BuildPartitionMatchPredicate(spec, schema, 
[]map[int]any{{1000: collisionBucket}})
+       require.NoError(t, err)
+
+       eval, err := iceberg.ExpressionEvaluator(schema, expr, true)
+       require.NoError(t, err)
+       for _, value := range collisionValues {
+               matched, err := eval(partitionPredicateRow{value})
+               require.NoError(t, err)
+               assert.True(t, matched, "source value %d should match bucket 
%d", value, collisionBucket)
+       }
+       matched, err := eval(partitionPredicateRow{nonMatchingValue})
+       require.NoError(t, err)
+       assert.False(t, matched, "source value %d should not match bucket %d", 
nonMatchingValue, collisionBucket)
+}
+
+func TestBuildPartitionMatchPredicate_EvaluatesTransformedNull(t *testing.T) {
+       cases := []struct {
+               name          string
+               sourceType    iceberg.Type
+               transform     iceberg.Transform
+               nonNil        any
+               nonNilMatches bool
+       }{
+               {name: "bucket", sourceType: iceberg.PrimitiveTypes.Int32, 
transform: iceberg.BucketTransform{NumBuckets: 4}, nonNil: int32(1), 
nonNilMatches: false},
+               {name: "truncate", sourceType: iceberg.PrimitiveTypes.String, 
transform: iceberg.TruncateTransform{Width: 3}, nonNil: "books", nonNilMatches: 
false},
+               {name: "year", sourceType: iceberg.PrimitiveTypes.Timestamp, 
transform: iceberg.YearTransform{}, nonNil: timestampAtUTC(2020, 1, 1, 0, 0, 0, 
0), nonNilMatches: false},
+               {name: "month", sourceType: iceberg.PrimitiveTypes.Timestamp, 
transform: iceberg.MonthTransform{}, nonNil: timestampAtUTC(2020, 1, 1, 0, 0, 
0, 0), nonNilMatches: false},
+               {name: "day", sourceType: iceberg.PrimitiveTypes.Timestamp, 
transform: iceberg.DayTransform{}, nonNil: timestampAtUTC(2020, 1, 1, 0, 0, 0, 
0), nonNilMatches: false},
+               {name: "hour", sourceType: iceberg.PrimitiveTypes.Timestamp, 
transform: iceberg.HourTransform{}, nonNil: timestampAtUTC(2020, 1, 1, 0, 0, 0, 
0), nonNilMatches: false},
+               {name: "void", sourceType: iceberg.PrimitiveTypes.String, 
transform: iceberg.VoidTransform{}, nonNil: "books", nonNilMatches: true},
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       schema := iceberg.NewSchema(0, iceberg.NestedField{
+                               ID: 1, Name: "value", Type: tc.sourceType,
+                       })
+                       spec := specWithFields(iceberg.PartitionField{
+                               SourceIDs: []int{1}, FieldID: 1000, Name: 
"value_part", Transform: tc.transform,
+                       })
+
+                       expr, err := BuildPartitionMatchPredicate(spec, schema, 
[]map[int]any{{1000: nil}})
+                       require.NoError(t, err)
+
+                       eval, err := iceberg.ExpressionEvaluator(schema, expr, 
true)
+                       require.NoError(t, err)
+
+                       matched, err := eval(partitionPredicateRow{nil})
+                       require.NoError(t, err)
+                       assert.True(t, matched, "null source value should match 
null partition value")
+
+                       matched, err = eval(partitionPredicateRow{tc.nonNil})
+                       require.NoError(t, err)
+                       assert.Equal(t, tc.nonNilMatches, matched, "unexpected 
match for non-null source value")
+               })
+       }
+}
+
+func TestBuildPartitionMatchPredicate_VoidBindsAlwaysTrue(t *testing.T) {
+       spec := specWithFields(iceberg.PartitionField{

Review Comment:
   **minor** — TestBuildPartitionMatchPredicate_VoidBindsAlwaysTrue is vacuous
   
   The test builds a void-only spec, calls BindExpr, and asserts the result 
equals AlwaysTrue{} — but BuildPartitionMatchPredicate already returns the 
concrete value iceberg.AlwaysTrue{} before BindExpr runs, so BindExpr is a 
no-op identity and the assertion cannot fail for any reason related to void 
handling. The same applies to the {name: "void", ..., nonNilMatches: true} case 
at :542, which only asserts that AlwaysTrue matches every row. Either rename to 
reflect what is actually covered (a source-less void field contributes no 
clause) or drop the BindExpr step.



##########
table/internal/partition_predicate.go:
##########
@@ -151,6 +196,110 @@ func BuildPartitionMatchPredicate(spec 
iceberg.PartitionSpec, schema *iceberg.Sc
        return result, nil
 }
 
+func partitionTerm(transform iceberg.Transform, name string) 
iceberg.UnboundTerm {
+       ref := iceberg.Reference(name)
+       if isIdentityTransform(transform) {
+               return ref
+       }
+       if isVoidTransform(transform) {
+               // Use the value form so binding the null predicate can fold it 
to
+               // AlwaysTrue. Pointer forms are accepted by the Transform 
interface too.
+               return iceberg.NewUnboundTransform(iceberg.VoidTransform{}, ref)
+       }
+
+       return iceberg.NewUnboundTransform(transform, ref)
+}
+
+func isIdentityTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.IdentityTransform:
+               return true
+       case *iceberg.IdentityTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func isVoidTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {

Review Comment:
   **minor** — Pointer-form void is accepted here but not by the upstream 
IsUnpartitioned guard the doc relies on
   
   The doc comment at :48 leans on 'dynamic partition overwrite rejects 
unpartitioned tables upstream', but PartitionSpec.IsUnpartitioned 
(partitions.go:756) matches only the value form VoidTransform, while 
isVoidTransform here deliberately accepts *VoidTransform too. A spec whose only 
field is &VoidTransform{} therefore passes the upstream 'is partitioned' guard 
and yields AlwaysTrue from this builder — a whole-table match. Not reachable 
from parsed metadata (ParseTransform returns value forms only), so this is 
hand-constructed-input territory; either narrow isVoidTransform to the value 
form or widen IsUnpartitioned so the two agree.



##########
table/internal/partition_predicate.go:
##########
@@ -39,46 +39,69 @@ import (
 // result is an OR across distinct partitions, each clause an AND across the
 // spec's fields:
 //
-//     source == value   when the partition value is present
-//     IsNaN(source)      when the value is a floating-point NaN (x == NaN is 
never true)
-//     IsNull(source)     when the partition value is absent or nil
+//     transform(source) == value when the partition value is present
+//     IsNaN(transform(source)) when the value is a floating-point NaN (x == 
NaN is never true)
+//     IsNull(transform(source)) when the partition value is absent or nil
 //

Review Comment:
   **minor** — Doc comment contradicts the new fail-closed behaviour for absent 
partition keys
   
   Line 44 still documents 'IsNull(transform(source)) when the partition value 
is absent or nil', but :127-130 now returns ErrInvalidArgument when the field 
id is absent from the tuple — that is the headline behaviour change of commit 
28628f7 and the doc was not updated with it. Fix the comment to say absent keys 
are rejected.



##########
table/evaluators_test.go:
##########
@@ -3100,6 +3100,21 @@ func TestManifestEvaluatorKeepsTransformedTerms(t 
*testing.T) {
        assert.True(t, keep)
 }
 
+func TestPrepareBatchFilterRejectsTransformedTerms(t *testing.T) {
+       schema := iceberg.NewSchema(1,

Review Comment:
   **nit** — TestPrepareBatchFilterRejectsTransformedTerms sits in the wrong 
test file
   
   prepareBatchFilter is defined in table/transaction.go:3356; its test was 
added to table/evaluators_test.go. The test itself is good and correctly pins 
the limitation the PR description depends on — it just belongs next to the code 
it covers.



##########
table/internal/partition_predicate.go:
##########
@@ -151,6 +196,110 @@ func BuildPartitionMatchPredicate(spec 
iceberg.PartitionSpec, schema *iceberg.Sc
        return result, nil
 }
 
+func partitionTerm(transform iceberg.Transform, name string) 
iceberg.UnboundTerm {
+       ref := iceberg.Reference(name)
+       if isIdentityTransform(transform) {
+               return ref
+       }
+       if isVoidTransform(transform) {
+               // Use the value form so binding the null predicate can fold it 
to
+               // AlwaysTrue. Pointer forms are accepted by the Transform 
interface too.
+               return iceberg.NewUnboundTransform(iceberg.VoidTransform{}, ref)
+       }
+
+       return iceberg.NewUnboundTransform(transform, ref)
+}
+
+func isIdentityTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.IdentityTransform:
+               return true
+       case *iceberg.IdentityTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func isVoidTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.VoidTransform:
+               return true
+       case *iceberg.VoidTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func isTruncateTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.TruncateTransform:
+               return true
+       case *iceberg.TruncateTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func validatePartitionValue(transform iceberg.Transform, resultType 
iceberg.Type, lit iceberg.Literal) (iceberg.Literal, error) {
+       normalized, err := lit.To(resultType)
+       if err != nil {
+               return nil, fmt.Errorf("%w: partition value type %s cannot be 
converted to transform result type %s: %v",
+                       iceberg.ErrInvalidArgument, lit.Type(), resultType, err)
+       }
+
+       switch normalized.(type) {
+       case iceberg.AboveMaxLiteral, iceberg.BelowMinLiteral:
+               return nil, fmt.Errorf("%w: partition value %s is outside 
transform result type %s",
+                       iceberg.ErrInvalidArgument, normalized, resultType)
+       }
+
+       switch t := transform.(type) {
+       case iceberg.BucketTransform:
+               if err := validateBucketPartitionValue(t.NumBuckets, 
normalized); err != nil {
+                       return nil, err
+               }
+       case *iceberg.BucketTransform:
+               if t == nil {
+                       return nil, fmt.Errorf("%w: bucket transform cannot be 
nil", iceberg.ErrInvalidArgument)
+               }
+               if err := validateBucketPartitionValue(t.NumBuckets, 
normalized); err != nil {
+                       return nil, err
+               }
+       case iceberg.VoidTransform, *iceberg.VoidTransform:
+               return nil, fmt.Errorf("%w: void transform only accepts a nil 
partition value", iceberg.ErrInvalidArgument)

Review Comment:
   **minor** — Three more unreachable defensive branches in 
validatePartitionValue / validateBucketPartitionValue
   
   (a) :271-272 'void transform only accepts a nil partition value' cannot fire 
— the caller at :150-153 already returns an error for every void field with a 
non-nil value. (b) :265-267 '*BucketTransform cannot be nil' cannot fire — 
UnboundTransform.Bind at :95 rejects typed-nil pointers via isNilTransform 
first. (c) :291-294 'numBuckets in [1, MaxInt32]' cannot fire — 
f.Transform.MarshalText() at :98 already calls 
BucketTransform.validateNumBuckets, which rejects the same two conditions. Drop 
these and rely on the single checks that already exist, so the reader is not 
led to believe there are two independent validation layers.



##########
table/internal/partition_predicate.go:
##########
@@ -151,6 +196,110 @@ func BuildPartitionMatchPredicate(spec 
iceberg.PartitionSpec, schema *iceberg.Sc
        return result, nil
 }
 
+func partitionTerm(transform iceberg.Transform, name string) 
iceberg.UnboundTerm {
+       ref := iceberg.Reference(name)
+       if isIdentityTransform(transform) {
+               return ref
+       }
+       if isVoidTransform(transform) {
+               // Use the value form so binding the null predicate can fold it 
to
+               // AlwaysTrue. Pointer forms are accepted by the Transform 
interface too.
+               return iceberg.NewUnboundTransform(iceberg.VoidTransform{}, ref)
+       }
+
+       return iceberg.NewUnboundTransform(transform, ref)
+}
+
+func isIdentityTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.IdentityTransform:
+               return true
+       case *iceberg.IdentityTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func isVoidTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.VoidTransform:
+               return true
+       case *iceberg.VoidTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func isTruncateTransform(transform iceberg.Transform) bool {
+       switch t := transform.(type) {
+       case iceberg.TruncateTransform:
+               return true
+       case *iceberg.TruncateTransform:
+               return t != nil
+       default:
+               return false
+       }
+}
+
+func validatePartitionValue(transform iceberg.Transform, resultType 
iceberg.Type, lit iceberg.Literal) (iceberg.Literal, error) {
+       normalized, err := lit.To(resultType)
+       if err != nil {
+               return nil, fmt.Errorf("%w: partition value type %s cannot be 
converted to transform result type %s: %v",
+                       iceberg.ErrInvalidArgument, lit.Type(), resultType, err)
+       }
+
+       switch normalized.(type) {
+       case iceberg.AboveMaxLiteral, iceberg.BelowMinLiteral:
+               return nil, fmt.Errorf("%w: partition value %s is outside 
transform result type %s",
+                       iceberg.ErrInvalidArgument, normalized, resultType)
+       }
+
+       switch t := transform.(type) {
+       case iceberg.BucketTransform:
+               if err := validateBucketPartitionValue(t.NumBuckets, 
normalized); err != nil {
+                       return nil, err
+               }
+       case *iceberg.BucketTransform:
+               if t == nil {
+                       return nil, fmt.Errorf("%w: bucket transform cannot be 
nil", iceberg.ErrInvalidArgument)
+               }
+               if err := validateBucketPartitionValue(t.NumBuckets, 
normalized); err != nil {
+                       return nil, err
+               }
+       case iceberg.VoidTransform, *iceberg.VoidTransform:
+               return nil, fmt.Errorf("%w: void transform only accepts a nil 
partition value", iceberg.ErrInvalidArgument)
+       }
+
+       if (isIdentityTransform(transform) || isTruncateTransform(transform)) 
&& !isNaN(normalized.Any()) {
+               applied := 
transform.Apply(iceberg.Optional[iceberg.Literal]{Valid: true, Val: normalized})

Review Comment:
   **nit** — PR body overstates the impossible-value validation
   
   The description says the change will 'fail closed for missing or impossible 
partition values', but the range/fixed-point validation at :259-281 covers only 
bucket, truncate, identity and void. Year/month/day/hour partition values get 
no plausibility check. This is harmless in practice (an impossible value yields 
a predicate that matches nothing, i.e. under-delete rather than over-delete), 
but the claim should be scoped to the transforms actually validated.



-- 
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]

Reply via email to