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.