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/terraform-provider-iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new d23f1c8  Reserved Field IDs (#124)
d23f1c8 is described below

commit d23f1c8425fa26553a724b324100261c9580b5ed
Author: Alex Stephen <[email protected]>
AuthorDate: Thu Oct 15 04:20:23 2026 -0700

    Reserved Field IDs (#124)
---
 internal/provider/resource_table.go      |  4 +-
 internal/provider/resource_table_test.go | 82 +++++++++++++++++++++++++++++++-
 internal/provider/table_schema.go        | 12 +++--
 internal/provider/table_schema_test.go   | 60 +++++++++++++++++++++++
 4 files changed, 152 insertions(+), 6 deletions(-)

diff --git a/internal/provider/resource_table.go 
b/internal/provider/resource_table.go
index e369d0a..529ce1e 100644
--- a/internal/provider/resource_table.go
+++ b/internal/provider/resource_table.go
@@ -204,7 +204,7 @@ func (r *icebergTableResource) Create(ctx context.Context, 
req resource.CreateRe
        // the schema for duplicate field IDs.
        schema.assignFieldIDs(0)
        if err := schema.validateFieldIDs(); err != nil {
-               resp.Diagnostics.AddError("duplicate field id", err.Error())
+               resp.Diagnostics.AddError("invalid field id", err.Error())
 
                return
        }
@@ -462,7 +462,7 @@ func (r *icebergTableResource) calculateSchemaUpdates(ctx 
context.Context, plan,
        planSchema.resolveFieldIDs(&stateSchema)
        planSchema.assignFieldIDs(int64(tbl.Metadata().LastColumnID()))
        if err := planSchema.validateFieldIDs(); err != nil {
-               diags.AddError("duplicate field id", err.Error())
+               diags.AddError("invalid field id", err.Error())
 
                return nil
        }
diff --git a/internal/provider/resource_table_test.go 
b/internal/provider/resource_table_test.go
index 7d3b908..20b9457 100644
--- a/internal/provider/resource_table_test.go
+++ b/internal/provider/resource_table_test.go
@@ -363,7 +363,7 @@ func TestAccIcebergTableDuplicateFieldIDs(t *testing.T) {
                        {
                                // Two fields sharing an explicit id must fail 
before commit.
                                Config:      
testAccIcebergTableDuplicateIDsConfig(providerCfg, tableName),
-                               ExpectError: regexp.MustCompile(`(?s)duplicate 
field id.*reuses id 1`),
+                               ExpectError: regexp.MustCompile(`(?s)invalid 
field id.*reuses id 1`),
                        },
                },
        })
@@ -398,6 +398,86 @@ resource "iceberg_table" "test" {
 `, tableName, dataType, dataRequired)
 }
 
+func testAccIcebergTableExtraFieldConfig(providerCfg, tableName string, 
extraID int64) string {
+       extra := ""
+       if extraID != 0 {
+               extra = fmt.Sprintf(`,
+      {
+        id       = %d
+        name     = "extra"
+        type     = "string"
+        required = false
+      }`, extraID)
+       }
+
+       return providerCfg + fmt.Sprintf(`
+resource "iceberg_namespace" "db_reserved" {
+  name = ["db_reserved"]
+}
+
+resource "iceberg_table" "test" {
+  namespace = iceberg_namespace.db_reserved.name
+  name      = "%s"
+  schema = {
+    fields = [
+      {
+        id       = 1
+        name     = "id"
+        type     = "long"
+        required = true
+      }%s
+    ]
+  }
+}
+`, tableName, extra)
+}
+
+func TestAccIcebergTableReservedFieldIDs(t *testing.T) {
+       catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
+       if catalogURI == "" {
+               catalogURI = "http://localhost:8181";
+       }
+
+       providerCfg := fmt.Sprintf(providerConfig, catalogURI)
+       tableName := "reserved_ids_table"
+
+       resource.Test(t, resource.TestCase{
+               PreCheck:                 func() { testAccPreCheck(t) },
+               ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+               Steps: []resource.TestStep{
+                       {
+                               Config: 
testAccIcebergTableExtraFieldConfig(providerCfg, tableName, 0),
+                               Check:  
resource.TestCheckResourceAttr("iceberg_table.test", "schema.id", "0"),
+                       },
+                       {
+                               // The spec reserves ids above 2147483447.
+                               Config:      
testAccIcebergTableExtraFieldConfig(providerCfg, tableName, 2147483448),
+                               ExpectError: regexp.MustCompile(`(?s)invalid 
field id.*id 2147483448`),
+                       },
+                       {
+                               Config:      
testAccIcebergTableExtraFieldConfig(providerCfg, tableName, 2147483647),
+                               ExpectError: regexp.MustCompile(`(?s)invalid 
field id.*id 2147483647`),
+                       },
+                       {
+                               // Neither rejected id reached the catalog.
+                               Config: 
testAccIcebergTableExtraFieldConfig(providerCfg, tableName, 0),
+                               Check: resource.ComposeTestCheckFunc(
+                                       
resource.TestCheckResourceAttr("iceberg_table.test", "schema.fields.#", "1"),
+                                       
resource.TestCheckResourceAttr("iceberg_table.test", "schema.id", "0"),
+                               ),
+                       },
+                       {
+                               // The highest non-reserved id is accepted.
+                               Config: 
testAccIcebergTableExtraFieldConfig(providerCfg, tableName, 2147483447),
+                               Check: resource.ComposeTestCheckFunc(
+                                       
resource.TestCheckResourceAttr("iceberg_table.test", "schema.fields.1.id", 
"2147483447"),
+                                       
resource.TestCheckResourceAttr("iceberg_table.test", "schema.id", "1"),
+                               ),
+                       },
+               },
+       })
+}
+
 func TestAccIcebergTableInvalidEvolution(t *testing.T) {
        catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
        if catalogURI == "" {
diff --git a/internal/provider/table_schema.go 
b/internal/provider/table_schema.go
index 1717b2a..9a345fb 100644
--- a/internal/provider/table_schema.go
+++ b/internal/provider/table_schema.go
@@ -239,9 +239,12 @@ func (s *icebergTableSchema) resolveFieldIDs(prior 
*icebergTableSchema) {
        apply("", s.Fields)
 }
 
-// validateFieldIDs rejects an id used by more than one field. Field, list
-// element, map key/value, and nested ids share one space; users may set them, 
so
-// collisions must fail before commit.
+// maxFieldID is the largest field ID a table schema may use.
+const maxFieldID = 2147483447
+
+// validateFieldIDs rejects an id outside the usable range or used by more than
+// one field. Field, list element, map key/value, and nested ids share one
+// space; users may set them, so bad ids must fail before commit.
 func (s *icebergTableSchema) validateFieldIDs() error {
        seen := map[int64]string{}
 
@@ -283,6 +286,9 @@ func claimFieldID(seen map[int64]string, id types.Int64, 
name string) error {
                return nil
        }
        v := id.ValueInt64()
+       if v < 0 || v > maxFieldID {
+               return fmt.Errorf("field %q has id %d; field ids must be 
between 1 and %d (higher ids are reserved by the Iceberg spec)", name, v, 
maxFieldID)
+       }
        if prev, ok := seen[v]; ok {
                return fmt.Errorf("field %q reuses id %d already assigned to 
%q", name, v, prev)
        }
diff --git a/internal/provider/table_schema_test.go 
b/internal/provider/table_schema_test.go
index f3cf20a..bcef71c 100644
--- a/internal/provider/table_schema_test.go
+++ b/internal/provider/table_schema_test.go
@@ -15,10 +15,12 @@
 package provider
 
 import (
+       "fmt"
        "strings"
        "testing"
 
        "github.com/apache/iceberg-go"
+       "github.com/hashicorp/terraform-plugin-framework/types"
 )
 
 // schemaOf builds a single-field schema for "data" with id 2, alongside a
@@ -181,3 +183,61 @@ func TestValidateSchemaEvolution(t *testing.T) {
                })
        }
 }
+
+func TestValidateFieldIDsReservedRange(t *testing.T) {
+       schemaWithIDs := func(ids ...int64) icebergTableSchema {
+               var s icebergTableSchema
+               for i, id := range ids {
+                       s.Fields = append(s.Fields, icebergTableSchemaField{
+                               ID:   types.Int64Value(id),
+                               Name: fmt.Sprintf("f%d", i),
+                               Type: "long",
+                       })
+               }
+
+               return s
+       }
+
+       tests := []struct {
+               name    string
+               schema  icebergTableSchema
+               wantErr string
+       }{
+               {name: "max usable id", schema: schemaWithIDs(1, 2147483447)},
+               {name: "first reserved id", schema: schemaWithIDs(1, 
2147483448), wantErr: "id 2147483448"},
+               {name: "last reserved id", schema: schemaWithIDs(1, 
2147483647), wantErr: "id 2147483647"},
+               {name: "negative id", schema: schemaWithIDs(1, -1), wantErr: 
"id -1"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := tt.schema.validateFieldIDs()
+                       if tt.wantErr == "" {
+                               if err != nil {
+                                       t.Fatalf("expected no error, got %v", 
err)
+                               }
+
+                               return
+                       }
+
+                       if err == nil {
+                               t.Fatalf("expected error containing %q, got 
nil", tt.wantErr)
+                       }
+                       if !strings.Contains(err.Error(), tt.wantErr) {
+                               t.Errorf("expected error containing %q, got 
%v", tt.wantErr, err)
+                       }
+               })
+       }
+}
+
+func TestAssignFieldIDsIntoReservedRange(t *testing.T) {
+       // The fresh id assigned after 2147483447 is reserved.
+       s := icebergTableSchema{Fields: []icebergTableSchemaField{
+               {ID: types.Int64Value(2147483447), Name: "a", Type: "long"},
+               {ID: types.Int64Null(), Name: "b", Type: "long"},
+       }}
+       s.assignFieldIDs(0)
+       if err := s.validateFieldIDs(); err == nil || 
!strings.Contains(err.Error(), "id 2147483448") {
+               t.Fatalf("expected reserved id error, got %v", err)
+       }
+}

Reply via email to