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


##########
types.go:
##########
@@ -444,15 +444,22 @@ func (l *ListType) String() string { return 
fmt.Sprintf("list<%s>", l.Element) }
 
 func (l *ListType) UnmarshalJSON(b []byte) error {
        aux := struct {
-               ID   int       `json:"element-id"`
+               ID   *int      `json:"element-id"`
                Elem typeIFace `json:"element"`
                Req  bool      `json:"element-required"`
        }{}
        if err := json.Unmarshal(b, &aux); err != nil {
                return err
        }
 
-       l.ElementID = aux.ID
+       if aux.ID == nil {
+               return fmt.Errorf("%w: field is missing required 'element-id' 
key in JSON", ErrInvalidSchema)
+       }
+       if *aux.ID == 0 {
+               return fmt.Errorf("%w: field 'element-id' must not be 0", 
ErrInvalidSchema)
+       }

Review Comment:
   The nil check here is right — it mirrors Java's `JsonUtil.getInt` existence 
check, so keep it. The `== 0` rejection is the blocker, though, and I'd drop it.
   
   The spec only requires field IDs to be unique in the table schema — it 
doesn't reserve 0 or require positivity, and Java, PyIceberg, and iceberg-rust 
all parse 0 without complaint. So this makes iceberg-go permanently unable to 
read otherwise-valid metadata any of them wrote with `element-id: 0`. It also 
breaks our own round-trip: `ListType.ElementID` is a plain `int`, so 
`json.Marshal(&iceberg.ListType{})` emits `element-id: 0` and this same method 
then refuses it.
   
   ```suggestion
        if aux.ID == nil {
                return fmt.Errorf("%w: field is missing required 'element-id' 
key in JSON", ErrInvalidSchema)
        }
   ```
   
   Reject 0 vs. accept it as the spec allows — wdyt?



##########
schema_test.go:
##########
@@ -442,6 +442,217 @@ func TestUnmarshalSchema(t *testing.T) {
        assert.True(t, tableSchemaSimple.Equals(&schema))
 }
 
+func TestUnmarshalSchemaRejectsDuplicateFieldIDs(t *testing.T) {
+       tests := []struct {
+               name     string
+               schema   string
+               contains string
+       }{
+               {
+                       name: "top level",
+                       schema: `{
+                               "type": "struct",
+                               "fields": [
+                                       {"id": 1, "name": "foo", "type": 
"string", "required": false},
+                                       {"id": 1, "name": "bar", "type": "int", 
"required": true}
+                               ],
+                               "schema-id": 1,
+                               "identifier-field-ids": []
+                       }`,
+                       contains: "multiple fields for id 1: foo and bar",
+               },
+               {
+                       name: "nested struct",
+                       schema: `{
+                               "type": "struct",
+                               "fields": [
+                                       {"id": 1, "name": "id", "type": "long", 
"required": true},
+                                       {
+                                               "id": 2,
+                                               "name": "payload",
+                                               "type": {
+                                                       "type": "struct",
+                                                       "fields": [
+                                                               {"id": 3, 
"name": "inner", "type": "string", "required": false},
+                                                               {"id": 3, 
"name": "inner_dup", "type": "int", "required": false}
+                                                       ]
+                                               },
+                                               "required": false
+                                       }
+                               ],
+                               "schema-id": 1,
+                               "identifier-field-ids": []
+                       }`,
+                       contains: "multiple fields for id 3: inner and 
inner_dup",
+               },
+               {
+                       name: "list element",
+                       schema: `{
+                               "type": "struct",
+                               "fields": [
+                                       {"id": 1, "name": "id", "type": "long", 
"required": true},
+                                       {
+                                               "id": 2,
+                                               "name": "items",
+                                               "type": {
+                                                       "type": "list",
+                                                       "element-id": 1,
+                                                       "element": "int",
+                                                       "element-required": 
false
+                                               },
+                                               "required": false
+                                       }
+                               ],
+                               "schema-id": 1,
+                               "identifier-field-ids": []
+                       }`,
+                       contains: "multiple fields for id 1: id and element",
+               },
+               {
+                       name: "map key",
+                       schema: `{
+                               "type": "struct",
+                               "fields": [
+                                       {"id": 1, "name": "id", "type": "long", 
"required": true},
+                                       {
+                                               "id": 2,
+                                               "name": "props",
+                                               "type": {
+                                                       "type": "map",
+                                                       "key-id": 1,
+                                                       "key": "string",
+                                                       "value-id": 3,
+                                                       "value": "int",
+                                                       "value-required": false
+                                               },
+                                               "required": false
+                                       }
+                               ],
+                               "schema-id": 1,
+                               "identifier-field-ids": []
+                       }`,
+                       contains: "multiple fields for id 1: id and key",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       var schema iceberg.Schema
+                       err := json.Unmarshal([]byte(tt.schema), &schema)
+                       require.ErrorIs(t, err, iceberg.ErrInvalidSchema)
+                       assert.ErrorContains(t, err, tt.contains)
+               })
+       }
+}
+
+func TestUnmarshalSchemaRejectsMissingOrZeroCollectionFieldIDs(t *testing.T) {
+       tests := []struct {
+               name     string
+               schema   string
+               contains string
+       }{
+               {
+                       name: "missing list element id",
+                       schema: `{
+                               "type": "struct",
+                               "fields": [
+                                       {
+                                               "id": 1,
+                                               "name": "items",
+                                               "type": {
+                                                       "type": "list",
+                                                       "element": "int",
+                                                       "element-required": 
false
+                                               },
+                                               "required": false
+                                       }
+                               ],
+                               "schema-id": 1,
+                               "identifier-field-ids": []
+                       }`,
+                       contains: "field is missing required 'element-id' key 
in JSON",
+               },
+               {
+                       name: "zero list element id",

Review Comment:
   Once the `== 0` checks come out, this row and "zero map value id" assert the 
wrong outcome — they'd need to flip to `require.NoError`. While here: the table 
never exercises zero map key-id or missing map value-id (both are real 
branches), and there's no positive case proving a valid non-zero list/map 
schema still unmarshals after the `*int` change — `tableSchemaNested` goes 
through the constructor, not JSON decode, so a regression there wouldn't be 
caught. I'd add a round-trip case asserting `require.NoError`.



##########
types.go:
##########
@@ -524,18 +531,31 @@ func (m *MapType) String() string {
 
 func (m *MapType) UnmarshalJSON(b []byte) error {
        aux := struct {
-               KeyID    int       `json:"key-id"`
+               KeyID    *int      `json:"key-id"`
                Key      typeIFace `json:"key"`
-               ValueID  int       `json:"value-id"`
+               ValueID  *int      `json:"value-id"`
                Value    typeIFace `json:"value"`
                ValueReq *bool     `json:"value-required"`
        }{}
        if err := json.Unmarshal(b, &aux); err != nil {
                return err
        }
 
-       m.KeyID, m.KeyType = aux.KeyID, aux.Key.Type
-       m.ValueID, m.ValueType = aux.ValueID, aux.Value.Type
+       if aux.KeyID == nil {
+               return fmt.Errorf("%w: field is missing required 'key-id' key 
in JSON", ErrInvalidSchema)
+       }
+       if *aux.KeyID == 0 {
+               return fmt.Errorf("%w: field 'key-id' must not be 0", 
ErrInvalidSchema)
+       }
+       if aux.ValueID == nil {
+               return fmt.Errorf("%w: field is missing required 'value-id' key 
in JSON", ErrInvalidSchema)
+       }
+       if *aux.ValueID == 0 {
+               return fmt.Errorf("%w: field 'value-id' must not be 0", 
ErrInvalidSchema)
+       }

Review Comment:
   Same blocker on the map side — keep both nil checks, drop the two `== 0` 
checks. Java's `mapFromJson` parses zero key-id/value-id fine, so rejecting 
them breaks reading Java/Py/Rust-written maps the same way. The suggestion also 
spaces the two guards out, which keeps nlreturn happy.
   
   ```suggestion
        if aux.KeyID == nil {
                return fmt.Errorf("%w: field is missing required 'key-id' key 
in JSON", ErrInvalidSchema)
        }
   
        if aux.ValueID == nil {
                return fmt.Errorf("%w: field is missing required 'value-id' key 
in JSON", ErrInvalidSchema)
        }
   ```



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