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


##########
partitions.go:
##########
@@ -469,6 +488,30 @@ func (ps *PartitionSpec) UnmarshalJSON(b []byte) error {
        return nil
 }
 
+func validatePartitionFields(fields []PartitionField) error {
+       names := make(map[string]struct{}, len(fields))
+       definitions := make(map[string]struct{}, len(fields))
+       for _, field := range fields {
+               if _, ok := names[field.Name]; ok {
+                       return fmt.Errorf("%w: duplicate partition name: %s", 
ErrInvalidPartitionSpec, field.Name)
+               }
+               names[field.Name] = struct{}{}
+
+               if _, ok := field.Transform.(VoidTransform); ok {
+                       continue
+               }
+
+               definition := fmt.Sprintf("%v:%s", field.SourceIDs, 
field.Transform)

Review Comment:
   This keys redundancy off `Transform.String()`, which assumes `String()` is 
injective per transform. The constructor path uses `Transform.Equals()`, so we 
now have two independent notions of "same field" that can drift — a shared 
helper both paths call would keep the semantics in one place.
   
   It also carries forward a pre-existing gap rather than adding one: Java's 
`dedupName()` collapses year/month/day/hour to `"time"` and rejects e.g. 
`year(ts)` + `month(ts)` on the same column, whereas both our paths accept it. 
Not this PR's job to fix, just flagging. (Minor: the `fmt.Sprintf` might trip 
perfsprint depending on the version pinned in CI — worth a quick check.)



##########
partitions_test.go:
##########
@@ -647,10 +647,73 @@ func TestPartitionFieldUnmarshalJSON(t *testing.T) {
                }`
                var field iceberg.PartitionField
                err := json.Unmarshal([]byte(jsonData), &field)
-               require.NoError(t, err)
-               assert.Zero(t, field.SourceID())
-               assert.Equal(t, 1003, field.FieldID)
-               assert.Equal(t, "void", field.Name)
-               assert.Equal(t, iceberg.VoidTransform{}, field.Transform)
+               require.ErrorIs(t, err, iceberg.ErrInvalidPartitionSpec)
+               assert.ErrorContains(t, err, "requires source-id or source-ids")

Review Comment:
   The subtest name still says "unmarshal with no source id" (describing the 
input) but the outcome flipped from pass to reject — I'd rename it to something 
like "...is rejected" so the intent is obvious.
   
   Also, whichever way the void decision lands above, there's no positive case 
that a void field *with* a source-id still unmarshals cleanly — worth adding 
one asserting `SourceID() == 1`.



##########
partitions.go:
##########
@@ -143,10 +145,21 @@ func (p *PartitionField) UnmarshalJSON(b []byte) error {
        } else {
                p.SourceIDs = []int{aux.SourceID}
        }
+       for _, sourceID := range p.SourceIDs {

Review Comment:
   `{"source-ids":[]}` slips past the else-if above (the key is present), then 
falls through to `p.SourceIDs = []int{aux.SourceID}` = `[]int{0}`, so this loop 
reports "must be positive: 0" — which points at the wrong thing; the real 
problem is the empty array. PyIceberg rejects this explicitly with "Empty 
source-ids is not allowed."
   
   The "empty source IDs" test asserts on that "must be positive" message, so 
it's green only because of this side effect — if we ever allowed source-id 0 
it'd silently regress. I'd add an explicit empty-array check with its own 
message and update the test to match.



##########
partitions.go:
##########
@@ -121,6 +121,8 @@ func (p *PartitionField) UnmarshalJSON(b []byte) error {
                if _, ok := raw["source-ids"]; ok {
                        return errors.New("partition field cannot contain both 
source-id and source-ids")

Review Comment:
   This one returns a bare `errors.New`, but the new branch just below wraps 
`ErrInvalidPartitionSpec`. So `errors.Is(err, ErrInvalidPartitionSpec)` is true 
for one source-id problem and false for the other, which callers will trip 
over. I'd wrap this one too while we're here.



##########
partitions.go:
##########
@@ -121,6 +121,8 @@ func (p *PartitionField) UnmarshalJSON(b []byte) error {
                if _, ok := raw["source-ids"]; ok {
                        return errors.New("partition field cannot contain both 
source-id and source-ids")
                }
+       } else if _, ok := raw["source-ids"]; !ok {
+               return fmt.Errorf("%w: partition field requires source-id or 
source-ids", ErrInvalidPartitionSpec)

Review Comment:
   This branch rejects any field carrying neither source-id nor source-ids, and 
the changed test now expects `void` with no source-id to fail. void is the 
tombstone we write for dropped partition fields, and we used to accept it 
without a source column — the old test asserted `field.SourceID() == 0` on 
exactly this input. So this turns a documented contract into a hard parse 
failure for any metadata that already has a source-id-less void field.
   
   @tanmayrauth already called this out and I agree it's the crux. The spec 
does cut the other way — Java's `buildFromJsonFields` and PyIceberg both 
require source-id — so I don't think "accept everything" is obviously right 
either. I'd make it an explicit decision: either a void carve-out that 
synthesizes a sentinel source-id when neither key is present, or keep the hard 
reject but land it with a changelog/migration note. Either is fine, but it 
shouldn't be a side effect of this branch. wdyt?



##########
table/metadata_internal_test.go:
##########
@@ -564,6 +564,25 @@ func TestRejectInvalidSchemaEntries(t *testing.T) {
        })
 }
 
+func TestRejectStructurallyInvalidHistoricalPartitionSpec(t *testing.T) {

Review Comment:
   This is named for historical specs but only exercises the negative spec-id 
rejection. The PR's rationale is that historical specs may still reference 
dropped source columns — that's the case I'd most want pinned down: load a 
two-spec history where the earlier spec has an identity transform on a column 
that's gone from the later schema, and assert it still parses. Right now 
nothing locks that in.



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