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)
+ }
+}