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


##########
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) {

Review Comment:
   This branch is dead with the current callers. `partitionTerm` is only 
reached for fields where `fr.isVoid` is false, and that's set precisely when 
`isVoidTransform` is true up in the field loop, so a void transform never flows 
down here.
   
   I'd drop the `isVoidTransform` arm and let it fall through to the identity 
check plus the `NewUnboundTransform` default. wdyt?



##########
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
 //
 // Duplicate tuples collapse to a single clause, and an empty input yields
 // AlwaysFalse (matching nothing). Callers are expected to pass a partitioned
 // spec; dynamic partition overwrite rejects unpartitioned tables upstream.
+// Void fields are also accepted when their source column has been dropped (the
+// spec represents those tombstones with source ID 0), because void always
+// produces a null partition value and does not need a source column.
 //
-// Because only identity transforms are accepted (see below), the partition
-// value equals the source-column value, so "source == value" selects exactly
-// the rows in that partition. Non-identity transforms (bucket, truncate, the
-// time transforms) cannot be matched by a source-column predicate and need
-// partition-level matching instead; they are rejected here and tracked as a
-// follow-up under issue #1215.
+// The transform is kept in the row predicate so the match is evaluated against
+// the partition value rather than comparing the post-transform value directly
+// with the source column. This is phase 1 of issue #1216. The current 
overwrite
+// path cannot execute non-identity predicates for partial-file rewrites 
because
+// source-column metrics are conservative and the Substrait row-filter 
converter
+// rejects transformed terms; partition-level strict matching is still needed
+// before this helper can drive those rewrites (tracked in issue #1215).
 func BuildPartitionMatchPredicate(spec iceberg.PartitionSpec, schema 
*iceberg.Schema, partitions []map[int]any) (iceberg.BooleanExpression, error) {
        type fieldRef struct {
-               id   int
-               name string
+               id         int
+               name       string
+               transform  iceberg.Transform
+               resultType iceberg.Type
+               isVoid     bool
        }
 
        var fields []fieldRef
        for _, f := range spec.Fields() {
-               if _, ok := f.Transform.(iceberg.IdentityTransform); !ok {
-                       return nil, fmt.Errorf("%w: dynamic partition overwrite 
supports identity-transform partition fields only, got %s on %q (tracked in 
https://github.com/apache/iceberg-go/issues/1215)",
-                               iceberg.ErrNotImplemented, f.Transform, f.Name)
-               }
-
-               // Identity transforms always have exactly one source column.
+               // Partition transforms currently have exactly one source 
column.
                if len(f.SourceIDs) != 1 {
-                       return nil, fmt.Errorf("%w: identity partition field %q 
must have exactly one source id, got %d",
+                       return nil, fmt.Errorf("%w: partition field %q must 
have exactly one source id, got %d",
                                iceberg.ErrInvalidArgument, f.Name, 
len(f.SourceIDs))
                }
+               if isVoidTransform(f.Transform) {
+                       // A void field can survive source-column removal as a 
source-less
+                       // tombstone. Its output is always null, so resolving 
or binding the
+                       // source would add no information and would reject 
source ID 0.
+                       if _, err := f.Transform.MarshalText(); err != nil {

Review Comment:
   This MarshalText call can't fail. `VoidTransform.MarshalText` always returns 
`("void", nil)`, so the error branch is unreachable.
   
   The twin call in the non-void path just below at line 98 has the same 
problem: anything that made it past `Bind` is already representable. I'd drop 
both error checks.



##########
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 {

Review Comment:
   Same story as `partitionTerm` here: both of these are unreachable. 
`validatePartitionValue` only runs after the `isVoid` guard, so the 
`VoidTransform` case a few lines down never fires, and a typed-nil 
`*BucketTransform` is already rejected by `Bind` before we get here (the 
typed-nil-bucket test confirms it), so the `t == nil` guard can't trip either.
   
   I'd drop both and keep just the value-form bucket case.



##########
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",

Review Comment:
   The inner conversion error goes in with `%v`, so it lands in the message 
text but not the error chain, and `errors.Is/As` can't reach the cause.
   
   I'd switch it to `%w`, or drop the fragment if only the sentinel is meant to 
be unwrappable.



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