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

zeroshade 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 e6b3f8a5c refactor(table)!: make reserved properties immutable before 
v1 (#2326)
e6b3f8a5c is described below

commit e6b3f8a5c3059776c4a397af4ffb6305ab699a44
Author: Minh Vu <[email protected]>
AuthorDate: Mon Oct 26 21:18:43 2026 +0100

    refactor(table)!: make reserved properties immutable before v1 (#2326)
    
    * refactor(table)!: make reserved property names immutable
    
    Return defensive reserved property lists so consumers cannot alter table 
metadata validation.
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * chore: resolve release notes conflict with current main
    
    * perf(table): avoid defensive-copy allocation in reserved property 
validation
    
    * style(table): gofmt reserved property array
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
    Co-authored-by: Matt Topol <[email protected]>
---
 table/metadata.go                       |  4 ++--
 table/metadata_builder_internal_test.go | 23 +++++++++++++++++++++--
 table/properties.go                     |  9 ++++++++-
 website/src/releases.md                 |  1 +
 4 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/table/metadata.go b/table/metadata.go
index b2911a95b..7ef3c66f7 100644
--- a/table/metadata.go
+++ b/table/metadata.go
@@ -1163,7 +1163,7 @@ func (b *MetadataBuilder) RemoveProperties(keys []string) 
error {
        if len(keys) == 0 {
                return nil
        }
-       for _, reserved := range ReservedProperties {
+       for _, reserved := range reservedProperties {
                if slices.Contains(keys, reserved) {
                        return fmt.Errorf("can't remove reserved property %s", 
reserved)
                }
@@ -1336,7 +1336,7 @@ func (b *MetadataBuilder) SetProperties(props 
iceberg.Properties) error {
                return nil
        }
 
-       for _, key := range ReservedProperties {
+       for _, key := range reservedProperties {
                if _, ok := props[key]; ok {
                        return fmt.Errorf("%w: can't set reserved property %s", 
iceberg.ErrInvalidArgument, key)
                }
diff --git a/table/metadata_builder_internal_test.go 
b/table/metadata_builder_internal_test.go
index 6f2464dcb..375ed99ef 100644
--- a/table/metadata_builder_internal_test.go
+++ b/table/metadata_builder_internal_test.go
@@ -2172,6 +2172,25 @@ func TestDefaultSpecCannotBeRemoved(t *testing.T) {
        require.ErrorContains(t, builder.RemovePartitionSpecs([]int{0}), "can't 
remove default partition spec with id 0")
 }
 
+func TestReservedPropertiesCannotBeMutated(t *testing.T) {
+       properties := ReservedProperties()
+       index := slices.Index(properties, PropertyFormatVersion)
+       require.NotEqual(t, -1, index)
+
+       properties[index] = "not-reserved"
+       require.Contains(t, ReservedProperties(), PropertyFormatVersion)
+       require.NotContains(t, ReservedProperties(), "not-reserved")
+
+       builder := builderWithoutChanges(2)
+       require.ErrorContains(t,
+               builder.SetProperties(iceberg.Properties{PropertyFormatVersion: 
"2"}),
+               "can't set reserved property "+PropertyFormatVersion)
+       require.ErrorContains(t,
+               builder.RemoveProperties([]string{PropertyFormatVersion}),
+               "can't remove reserved property "+PropertyFormatVersion)
+       require.NoError(t, 
builder.SetProperties(iceberg.Properties{"not-reserved": "allowed"}))
+}
+
 func TestSetReservedPropertiesFails(t *testing.T) {
        builder := builderWithoutChanges(2)
 
@@ -2184,7 +2203,7 @@ func TestSetReservedPropertiesFails(t *testing.T) {
        require.True(t, builder.HasChanges())
 
        // Test setting each reserved property individually
-       for _, reserved := range ReservedProperties {
+       for _, reserved := range ReservedProperties() {
                err := builder.SetProperties(iceberg.Properties{reserved: 
"some-value"})
                require.ErrorContains(t, err, "can't set reserved property 
"+reserved)
        }
@@ -2245,7 +2264,7 @@ func TestRemoveReservedPropertiesFails(t *testing.T) {
        builder := builderWithoutChanges(2)
 
        // Test removing each reserved property individually
-       for _, reserved := range ReservedProperties {
+       for _, reserved := range ReservedProperties() {
                err := builder.RemoveProperties([]string{reserved})
                require.ErrorContains(t, err, "can't remove reserved property 
"+reserved)
        }
diff --git a/table/properties.go b/table/properties.go
index 46eb237a4..bfb252c1a 100644
--- a/table/properties.go
+++ b/table/properties.go
@@ -19,6 +19,7 @@ package table
 
 import (
        "math"
+       "slices"
        "strings"
 
        "github.com/apache/iceberg-go"
@@ -233,7 +234,7 @@ const (
        PropertyDefaultSortOrder         = "default-sort-order"
 )
 
-var ReservedProperties = [9]string{
+var reservedProperties = [...]string{
        PropertyFormatVersion,
        PropertyUuid,
        PropertySnapshotCount,
@@ -245,6 +246,12 @@ var ReservedProperties = [9]string{
        PropertyDefaultSortOrder,
 }
 
+// ReservedProperties returns a new slice of reserved table property names.
+// Mutating the returned slice does not change metadata validation.
+func ReservedProperties() []string {
+       return slices.Clone(reservedProperties[:])
+}
+
 // Metadata compression codecs
 const (
        MetadataCompressionCodecNone = "none"
diff --git a/website/src/releases.md b/website/src/releases.md
index 4364cda9a..2af23acce 100644
--- a/website/src/releases.md
+++ b/website/src/releases.md
@@ -42,6 +42,7 @@ Per-release notes (highlights, breaking changes, 
contributors) are published on
 
 ### Unreleased compatibility notes
 
+- **Reserved table properties now use a read-only API.** Replace references to 
`table.ReservedProperties` with `table.ReservedProperties()`. The function 
returns a fresh slice, so callers cannot change metadata validation by 
modifying it.
 - **Fixed and decimal constructors return errors.** `iceberg.FixedTypeOf` and 
`iceberg.DecimalTypeOf` now return `(type, error)` instead of panicking for 
invalid inputs. Use `MustFixedTypeOf` or `MustDecimalTypeOf` only for 
known-valid values.
 - **SQL UDF representations now use struct literals.** The 
`udf.NewSQLRepresentation(dialect, sql)` function, available since v0.7.0, is 
removed. Use `udf.SQLRepresentation{Dialect: "trino", SQL: "x + 1"}` instead. 
Empty fields and duplicate dialects are checked during metadata validation; 
serialized JSON is unchanged.
 - **Filesystem adapter construction consolidated.** Replace 
`io.FSPreProcName(fsys, fn)` with `io.FS(fsys, io.WithNamePreprocessor(fn))`. 
Plain `io.FS(fsys)` remains supported.

Reply via email to