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


##########
expr_json.go:
##########
@@ -579,6 +579,11 @@ func decodeTerm(raw json.RawMessage) (UnboundTerm, error) {
                if err != nil {
                        return nil, fmt.Errorf("%w: cannot parse transform 
term: %s", ErrInvalidArgument, err)
                }
+               // Unknown transforms are tolerated in partition/sort metadata, 
but a
+               // filter expression referencing one can't be evaluated.
+               if _, ok := tf.(UnknownTransform); ok {

Review Comment:
   This rejection has no test coverage at all. It is a small addition, but it 
is the only thing standing between an unknown transform and a malformed REST 
filter payload.
   
   Suggested fix: add a `decodeTerm` case feeding a term whose transform is 
unknown and assert `ErrInvalidArgument`.



##########
transforms.go:
##########
@@ -230,6 +246,49 @@ func (VoidTransform) Project(string, BoundPredicate) 
(UnboundPredicate, error) {
        return nil, nil
 }
 
+// UnknownTransform is a placeholder for a partition or sort transform that
+// this implementation doesn't recognize. The v3 spec requires readers to load
+// tables that use unknown transforms and to ignore those fields when
+// filtering; writers must not commit a partition spec that uses one.
+type UnknownTransform struct {
+       name string
+}
+
+func (t UnknownTransform) MarshalText() ([]byte, error) {
+       return []byte(t.name), nil
+}
+
+func (t UnknownTransform) String() string { return t.name }
+
+// CanTransform assumes an unknown transform could apply to any type -- the
+// real applicability isn't known.
+func (UnknownTransform) CanTransform(Type) bool { return true }

Review Comment:
   Non-blocking: because `CanTransform` is unconditionally true, 
`SortOrder.CheckCompatibility` silently accepts any source type for an unknown 
transform. That is the right runtime behavior — we genuinely cannot know — but 
the doc comment reads as if compatibility were verified.
   
   Suggested fix: note on this method that compatibility is *unverifiable* for 
unknown transforms, not verified.



##########
transforms.go:
##########
@@ -230,6 +246,49 @@ func (VoidTransform) Project(string, BoundPredicate) 
(UnboundPredicate, error) {
        return nil, nil
 }
 
+// UnknownTransform is a placeholder for a partition or sort transform that
+// this implementation doesn't recognize. The v3 spec requires readers to load
+// tables that use unknown transforms and to ignore those fields when
+// filtering; writers must not commit a partition spec that uses one.
+type UnknownTransform struct {
+       name string
+}
+
+func (t UnknownTransform) MarshalText() ([]byte, error) {
+       return []byte(t.name), nil
+}
+
+func (t UnknownTransform) String() string { return t.name }
+
+// CanTransform assumes an unknown transform could apply to any type -- the
+// real applicability isn't known.
+func (UnknownTransform) CanTransform(Type) bool { return true }
+
+// ResultType is unknown, so report string, matching the Java reference.
+func (UnknownTransform) ResultType(Type) Type { return StringType{} }
+
+func (UnknownTransform) PreservesOrder() bool { return false }
+
+func (t UnknownTransform) Equals(other Transform) bool {
+       o, ok := other.(UnknownTransform)
+
+       return ok && t.name == o.name
+}
+
+// Apply can't be evaluated for an unknown transform.
+func (UnknownTransform) Apply(Optional[Literal]) Optional[Literal] {
+       return Optional[Literal]{}
+}
+
+func (t UnknownTransform) ToHumanStr(any) string { return t.name }
+
+func (t UnknownTransform) ToHumanStrType(Type, any) string { return t.name }

Review Comment:
   **Blocking (part of the position-delete issue in the summary).** 
`ToHumanStrType` and `ToHumanStr` (`:283`) return the transform *name*, so they 
are constant for a given spec field regardless of the value passed in.
   
   `PartitionSpec.PartitionToPath` (`partitions.go:710`) feeds this straight 
into the writer key at `table/rolling_data_writer.go:348`. Two genuinely 
different partition values therefore produce the identical path string, the map 
lookup returns the writer created for the first one, and the new 
`partitionValues` are discarded. The same collapse shows up in 
`table/snapshots.go:459,474`, where `partition-summaries` and 
`changed-partition-count` come out wrong.
   
   Suggested fix: render the value rather than the transform name. Java's 
`Transform#toHumanString` default renders the value and `UnknownTransform` does 
not override it, so matching that behavior also fixes the summary counts and 
makes it impossible for any future `PartitionToPath` caller to collide.



##########
transforms.go:
##########
@@ -230,6 +246,49 @@ func (VoidTransform) Project(string, BoundPredicate) 
(UnboundPredicate, error) {
        return nil, nil
 }
 
+// UnknownTransform is a placeholder for a partition or sort transform that
+// this implementation doesn't recognize. The v3 spec requires readers to load
+// tables that use unknown transforms and to ignore those fields when
+// filtering; writers must not commit a partition spec that uses one.
+type UnknownTransform struct {

Review Comment:
   Non-blocking: `iceberg.UnknownTransform{}` is a legal composite literal from 
outside the package — all fields being unexported makes them unsettable, not 
the struct unconstructible. That yields `name == ""`, which marshals to 
`"transform": ""` and produces metadata that cannot be read back. `MarshalText` 
returning no error also bypasses the `newSortOrder` backstop at 
`table/sorting.go:321`.
   
   Suggested fix: reject empty names in `validateTransform` 
(`partitions.go:314`), or return an error from `MarshalText` when `name` is 
empty.



##########
table/sorting.go:
##########
@@ -249,6 +253,11 @@ func (s SortOrder) Len() int {
        return len(s.fields)
 }
 
+// Field returns the sort field at index i.
+func (s SortOrder) Field(i int) SortField {

Review Comment:
   Non-blocking: the new `Field(i)` returns `s.fields[i]` raw while `Fields()` 
(`:242`) clones. `SortField.SourceIDs` is a slice, so a caller can mutate the 
sort order's internals through the returned value — which breaks exactly the 
invariant `TestSortOrderReturnsDefensiveCopies` asserts for `Fields()`.
   
   Suggested fix: return `cloneSortField(s.fields[i])`, and document that an 
out-of-range index panics.



##########
table/update_spec.go:
##########
@@ -86,6 +86,12 @@ func NewUpdateSpec(t *Transaction, caseSensitive bool) 
*UpdateSpec {
        nameToField := make(map[string]iceberg.PartitionField)
        partitionSpec := t.tbl.Metadata().PartitionSpec()
        for _, partitionField := range partitionSpec.Fields() {
+               if _, ok := 
partitionField.Transform.(iceberg.UnknownTransform); ok {

Review Comment:
   Worth surfacing to users: this blanket rejection blocks *all* spec evolution 
on a table with an unknown transform, including unrelated renames and drops. 
You confirmed this matches Java and it is the safer default, so no change to 
the behavior — but the rationale currently lives only in 
`update_spec_test.go:64`.
   
   Suggested fix: move that reasoning into the `NewUpdateSpec` doc comment, 
where someone hitting the error will actually find it.



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