This is an automated email from the ASF dual-hosted git repository.

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new ded986b42 refactor(table): deduplicate schema default validation 
(#2298)
ded986b42 is described below

commit ded986b4233a6c548655973d851b9765bdf5cdfe
Author: Minh Vu <[email protected]>
AuthorDate: Sun Oct 25 10:10:22 2026 +0100

    refactor(table): deduplicate schema default validation (#2298)
---
 table/metadata_schema_compatibility.go      | 179 ++++++++++--------------
 table/metadata_schema_compatibility_test.go | 208 ++++++++++++++++++++++++++++
 2 files changed, 281 insertions(+), 106 deletions(-)

diff --git a/table/metadata_schema_compatibility.go 
b/table/metadata_schema_compatibility.go
index e10ed0aa8..62599575e 100644
--- a/table/metadata_schema_compatibility.go
+++ b/table/metadata_schema_compatibility.go
@@ -270,106 +270,75 @@ func typeRequiresNullDefaults(t iceberg.Type) bool {
        }
 }
 
+// isRequiredUnknownField identifies unknown fields that violate the 
optionality
+// rule, regardless of whether they appear in a struct, list, or map.
+func isRequiredUnknownField(field iceberg.NestedField) bool {
+       _, isUnknown := field.Type.(iceberg.UnknownType)
+
+       return isUnknown && field.Required
+}
+
+// validateUnknownFieldDefaults keeps the null-default rule for unknown and
+// variant types in one place for all schema container types.
+func validateUnknownFieldDefaults(field iceberg.NestedField) error {
+       if !typeRequiresNullDefaults(field.Type) {
+               return nil
+       }
+       if field.InitialDefault != nil {
+               return fmt.Errorf("%s type field '%s' (id: %d) must have null 
initial-default, but got: %v",
+                       field.Type, field.Name, field.ID, field.InitialDefault)
+       }
+       if field.WriteDefault != nil {
+               return fmt.Errorf("%s type field '%s' (id: %d) must have null 
write-default, but got: %v",
+                       field.Type, field.Name, field.ID, field.WriteDefault)
+       }
+
+       return nil
+}
+
+// Nested list and map fields have historically used slightly different
+// wording from struct fields when reporting a required unknown type.
+func validateNestedUnknownField(field iceberg.NestedField) error {
+       if isRequiredUnknownField(field) {
+               return fmt.Errorf("unknown type field '%s' (id: %d) must be 
optional, but was marked required",
+                       field.Name, field.ID)
+       }
+
+       return validateUnknownFieldDefaults(field)
+}
+
 func (v *unknownTypeValidator) Field(field iceberg.NestedField, fieldResult 
error) error {
        if fieldResult != nil {
                return fieldResult
        }
-       // Optionality: unknown must be optional per spec; variant has no such 
constraint.
-       // Both require null defaults (enforced below via 
typeRequiresNullDefaults).
-       if _, isUnknown := field.Type.(iceberg.UnknownType); isUnknown {
-               if field.Required {
-                       return fmt.Errorf("unknown type field '%s' (id: %d) 
must be optional, but was marked as required",
-                               field.Name, field.ID)
-               }
-       }
-       if typeRequiresNullDefaults(field.Type) {
-               if field.InitialDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null initial-default, but got: %v",
-                               field.Type, field.Name, field.ID, 
field.InitialDefault)
-               }
-               if field.WriteDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null write-default, but got: %v",
-                               field.Type, field.Name, field.ID, 
field.WriteDefault)
-               }
+       if isRequiredUnknownField(field) {
+               return fmt.Errorf("unknown type field '%s' (id: %d) must be 
optional, but was marked as required",
+                       field.Name, field.ID)
        }
 
-       return nil
+       return validateUnknownFieldDefaults(field)
 }
 
 func (v *unknownTypeValidator) List(list iceberg.ListType, elemResult error) 
error {
        if elemResult != nil {
                return elemResult
        }
-       elem := list.ElementField()
-
-       if _, isUnknown := elem.Type.(iceberg.UnknownType); isUnknown {
-               if elem.Required {
-                       return fmt.Errorf("unknown type field '%s' (id: %d) 
must be optional, but was marked required",
-                               elem.Name, elem.ID)
-               }
-       }
-       if typeRequiresNullDefaults(elem.Type) {
-               if elem.InitialDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null initial-default, but got: %v",
-                               elem.Type, elem.Name, elem.ID, 
elem.InitialDefault)
-               }
-               if elem.WriteDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null write-default, but got: %v",
-                               elem.Type, elem.Name, elem.ID, 
elem.WriteDefault)
-               }
-       }
 
-       return nil
+       return validateNestedUnknownField(list.ElementField())
 }
 
 func (v *unknownTypeValidator) Map(mapType iceberg.MapType, keyResult, 
valueResult error) error {
        if keyResult != nil {
                return keyResult
        }
-
        if valueResult != nil {
                return valueResult
        }
-
-       key := mapType.KeyField()
-
-       if _, isKeyUnknown := key.Type.(iceberg.UnknownType); isKeyUnknown {
-               if key.Required {
-                       return fmt.Errorf("unknown type field '%s' (id: %d) 
must be optional, but was marked required",
-                               key.Name, key.ID)
-               }
-       }
-       if typeRequiresNullDefaults(key.Type) {
-               if key.InitialDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null initial-default, but got: %v",
-                               key.Type, key.Name, key.ID, key.InitialDefault)
-               }
-               if key.WriteDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null write-default, but got: %v",
-                               key.Type, key.Name, key.ID, key.WriteDefault)
-               }
-       }
-
-       value := mapType.ValueField()
-
-       if _, isValueUnknown := value.Type.(iceberg.UnknownType); 
isValueUnknown {
-               if value.Required {
-                       return fmt.Errorf("unknown type field '%s' (id: %d) 
must be optional, but was marked required",
-                               value.Name, value.ID)
-               }
-       }
-       if typeRequiresNullDefaults(value.Type) {
-               if value.InitialDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null initial-default, but got: %v",
-                               value.Type, value.Name, value.ID, 
value.InitialDefault)
-               }
-               if value.WriteDefault != nil {
-                       return fmt.Errorf("%s type field '%s' (id: %d) must 
have null write-default, but got: %v",
-                               value.Type, value.Name, value.ID, 
value.WriteDefault)
-               }
+       if err := validateNestedUnknownField(mapType.KeyField()); err != nil {
+               return err
        }
 
-       return nil
+       return validateNestedUnknownField(mapType.ValueField())
 }
 
 func (v *unknownTypeValidator) Primitive(_ iceberg.PrimitiveType) error {
@@ -447,40 +416,38 @@ func (v *complexTypeDefaultValidator) Variant(_ 
iceberg.VariantType) error {
 }
 
 func validateComplexDefault(field iceberg.NestedField) error {
+       if err := validateComplexDefaultValue(field, "initial-default", 
field.InitialDefault); err != nil {
+               return err
+       }
+
+       return validateComplexDefaultValue(field, "write-default", 
field.WriteDefault)
+}
+
+// validateComplexDefaultValue checks the expected JSON shape without
+// duplicating the initial-default and write-default error handling.
+func validateComplexDefaultValue(field iceberg.NestedField, name string, value 
any) error {
+       if value == nil {
+               return nil
+       }
+
+       var typeName, expectedShape string
+       var valid bool
        switch field.Type.(type) {
        case *iceberg.StructType:
-               if field.InitialDefault != nil {
-                       if _, ok := field.InitialDefault.(map[string]any); !ok {
-                               return fmt.Errorf("struct type field '%s' (id: 
%d) must have null or JSON object initial-default, but got: %v", field.Name, 
field.ID, field.InitialDefault)
-                       }
-               }
-               if field.WriteDefault != nil {
-                       if _, ok := field.WriteDefault.(map[string]any); !ok {
-                               return fmt.Errorf("struct type field '%s' (id: 
%d) must have null or JSON object write-default, but got: %v", field.Name, 
field.ID, field.WriteDefault)
-                       }
-               }
+               typeName, expectedShape = "struct", "JSON object"
+               _, valid = value.(map[string]any)
        case *iceberg.ListType:
-               if field.InitialDefault != nil {
-                       if _, ok := field.InitialDefault.([]any); !ok {
-                               return fmt.Errorf("list type field '%s' (id: 
%d) must have null or JSON array initial-default, but got: %v", field.Name, 
field.ID, field.InitialDefault)
-                       }
-               }
-               if field.WriteDefault != nil {
-                       if _, ok := field.WriteDefault.([]any); !ok {
-                               return fmt.Errorf("list type field '%s' (id: 
%d) must have null or JSON array write-default, but got: %v", field.Name, 
field.ID, field.WriteDefault)
-                       }
-               }
+               typeName, expectedShape = "list", "JSON array"
+               _, valid = value.([]any)
        case *iceberg.MapType:
-               if field.InitialDefault != nil {
-                       if _, ok := field.InitialDefault.(map[string]any); !ok {
-                               return fmt.Errorf("map type field '%s' (id: %d) 
must have null or JSON object initial-default, but got: %v", field.Name, 
field.ID, field.InitialDefault)
-                       }
-               }
-               if field.WriteDefault != nil {
-                       if _, ok := field.WriteDefault.(map[string]any); !ok {
-                               return fmt.Errorf("map type field '%s' (id: %d) 
must have null or JSON object write-default, but got: %v", field.Name, 
field.ID, field.WriteDefault)
-                       }
-               }
+               typeName, expectedShape = "map", "JSON object"
+               _, valid = value.(map[string]any)
+       default:
+               return nil
+       }
+       if !valid {
+               return fmt.Errorf("%s type field '%s' (id: %d) must have null 
or %s %s, but got: %v",
+                       typeName, field.Name, field.ID, expectedShape, name, 
value)
        }
 
        return nil
diff --git a/table/metadata_schema_compatibility_test.go 
b/table/metadata_schema_compatibility_test.go
index 2fc5aca7a..cb665bea1 100644
--- a/table/metadata_schema_compatibility_test.go
+++ b/table/metadata_schema_compatibility_test.go
@@ -50,3 +50,211 @@ func TestNewMetadata_DuplicateValues(t *testing.T) {
        assert.ErrorIs(t, err, iceberg.ErrInvalidSchema)
        assert.Contains(t, err.Error(), "multiple fields for name foo")
 }
+
+func TestUnknownTypeValidatorRetainsErrorBehavior(t *testing.T) {
+       validator := &unknownTypeValidator{}
+       tests := []struct {
+               name     string
+               validate func() error
+               want     string
+       }{
+               {
+                       name: "struct field requires optional unknown",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 1, Name: "field", Type: 
iceberg.UnknownType{}, Required: true,
+                               }, nil)
+                       },
+                       want: "unknown type field 'field' (id: 1) must be 
optional, but was marked as required",
+               },
+               {
+                       name: "list element requires optional unknown",
+                       validate: func() error {
+                               return validator.List(iceberg.ListType{
+                                       ElementID: 2, Element: 
iceberg.UnknownType{}, ElementRequired: true,
+                               }, nil)
+                       },
+                       want: "unknown type field 'element' (id: 2) must be 
optional, but was marked required",
+               },
+               {
+                       name: "map key requires optional unknown",
+                       validate: func() error {
+                               return validator.Map(iceberg.MapType{
+                                       KeyID: 3, KeyType: 
iceberg.UnknownType{},
+                                       ValueID: 4, ValueType: 
iceberg.StringType{},
+                               }, nil, nil)
+                       },
+                       want: "unknown type field 'key' (id: 3) must be 
optional, but was marked required",
+               },
+               {
+                       name: "map value requires optional unknown",
+                       validate: func() error {
+                               return validator.Map(iceberg.MapType{
+                                       KeyID: 3, KeyType: iceberg.StringType{},
+                                       ValueID: 4, ValueType: 
iceberg.UnknownType{}, ValueRequired: true,
+                               }, nil, nil)
+                       },
+                       want: "unknown type field 'value' (id: 4) must be 
optional, but was marked required",
+               },
+               {
+                       name: "unknown initial default is rejected",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 5, Name: "field", Type: 
iceberg.UnknownType{}, InitialDefault: "wrong",
+                               }, nil)
+                       },
+                       want: "unknown type field 'field' (id: 5) must have 
null initial-default, but got: wrong",
+               },
+               {
+                       name: "unknown write default is rejected",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 5, Name: "field", Type: 
iceberg.UnknownType{}, WriteDefault: "wrong",
+                               }, nil)
+                       },
+                       want: "unknown type field 'field' (id: 5) must have 
null write-default, but got: wrong",
+               },
+               {
+                       name: "variant initial default is rejected",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 5, Name: "field", Type: 
iceberg.VariantType{}, InitialDefault: "wrong",
+                               }, nil)
+                       },
+                       want: "variant type field 'field' (id: 5) must have 
null initial-default, but got: wrong",
+               },
+               {
+                       name: "variant write default is rejected",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 5, Name: "field", Type: 
iceberg.VariantType{}, WriteDefault: "wrong",
+                               }, nil)
+                       },
+                       want: "variant type field 'field' (id: 5) must have 
null write-default, but got: wrong",
+               },
+               {
+                       name: "required variant is allowed",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 5, Name: "field", Type: 
iceberg.VariantType{}, Required: true,
+                               }, nil)
+                       },
+               },
+               {
+                       name: "regular types retain non-null defaults",
+                       validate: func() error {
+                               return validator.Field(iceberg.NestedField{
+                                       ID: 5, Name: "field", Type: 
iceberg.StringType{},
+                                       InitialDefault: "initial", 
WriteDefault: "written",
+                               }, nil)
+                       },
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := tt.validate()
+                       if tt.want == "" {
+                               require.NoError(t, err)
+                       } else {
+                               require.EqualError(t, err, tt.want)
+                       }
+               })
+       }
+}
+
+func TestUnknownTypeValidatorPreservesChildErrorPriority(t *testing.T) {
+       validator := &unknownTypeValidator{}
+       childErr := iceberg.ErrInvalidSchema
+       require.ErrorIs(t, validator.Field(iceberg.NestedField{
+               ID: 1, Name: "field", Type: iceberg.UnknownType{}, Required: 
true,
+       }, childErr), childErr)
+       require.ErrorIs(t, validator.List(iceberg.ListType{
+               ElementID: 2, Element: iceberg.UnknownType{}, ElementRequired: 
true,
+       }, childErr), childErr)
+
+       mapType := iceberg.MapType{
+               KeyID: 3, KeyType: iceberg.UnknownType{},
+               ValueID: 4, ValueType: iceberg.UnknownType{}, ValueRequired: 
true,
+       }
+       require.ErrorIs(t, validator.Map(mapType, childErr, nil), childErr)
+       require.ErrorIs(t, validator.Map(mapType, nil, childErr), childErr)
+}
+
+func TestValidateComplexDefaultRetainsShapeRules(t *testing.T) {
+       tests := []struct {
+               name    string
+               typ     iceberg.Type
+               initial any
+               write   any
+               want    string
+       }{
+               {
+                       name: "struct initial default must be object",
+                       typ:  &iceberg.StructType{}, initial: "wrong",
+                       want: "struct type field 'field' (id: 5) must have null 
or JSON object initial-default, but got: wrong",
+               },
+               {
+                       name: "struct write default must be object",
+                       typ:  &iceberg.StructType{}, write: "wrong",
+                       want: "struct type field 'field' (id: 5) must have null 
or JSON object write-default, but got: wrong",
+               },
+               {
+                       name: "list initial default must be array",
+                       typ:  &iceberg.ListType{}, initial: "wrong",
+                       want: "list type field 'field' (id: 5) must have null 
or JSON array initial-default, but got: wrong",
+               },
+               {
+                       name: "list write default must be array",
+                       typ:  &iceberg.ListType{}, write: "wrong",
+                       want: "list type field 'field' (id: 5) must have null 
or JSON array write-default, but got: wrong",
+               },
+               {
+                       name: "map initial default must be object",
+                       typ:  &iceberg.MapType{}, initial: "wrong",
+                       want: "map type field 'field' (id: 5) must have null or 
JSON object initial-default, but got: wrong",
+               },
+               {
+                       name: "map write default must be object",
+                       typ:  &iceberg.MapType{}, write: "wrong",
+                       want: "map type field 'field' (id: 5) must have null or 
JSON object write-default, but got: wrong",
+               },
+               {
+                       name: "initial default error takes priority",
+                       typ:  &iceberg.ListType{}, initial: "initial", write: 
"write",
+                       want: "list type field 'field' (id: 5) must have null 
or JSON array initial-default, but got: initial",
+               },
+               {
+                       name:    "struct accepts object defaults",
+                       typ:     &iceberg.StructType{},
+                       initial: map[string]any{"a": 1}, write: 
map[string]any(nil),
+               },
+               {
+                       name:    "list accepts array defaults",
+                       typ:     &iceberg.ListType{},
+                       initial: []any{"a"}, write: []any(nil),
+               },
+               {
+                       name:    "map accepts object defaults",
+                       typ:     &iceberg.MapType{},
+                       initial: map[string]any{"a": 1}, write: 
map[string]any{"b": 2},
+               },
+               {
+                       name: "primitive defaults unchanged",
+                       typ:  iceberg.StringType{}, initial: "initial", write: 
"write",
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       field := iceberg.NestedField{
+                               ID: 5, Name: "field", Type: tt.typ,
+                               InitialDefault: tt.initial, WriteDefault: 
tt.write,
+                       }
+                       err := validateComplexDefault(field)
+                       if tt.want == "" {
+                               require.NoError(t, err)
+                       } else {
+                               require.EqualError(t, err, tt.want)
+                       }
+               })
+       }
+}

Reply via email to