You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2026/08/14 16:17:52 UTC

(iceberg-go) branch main updated: Namespace properties conformance tests (#1795)

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 85d517e5d Namespace properties conformance tests (#1795)
85d517e5d is described below

commit 85d517e5dc8c601b1b405ebb67b3e0d9391f3596
Author: Alex Stephen <13...@users.noreply.github.com>
AuthorDate: Fri Aug 14 09:17:47 2026 -0700

    Namespace properties conformance tests (#1795)
---
 catalog/catalogtest/catalogtest.go |  16 ++++
 catalog/catalogtest/namespaces.go  | 189 +++++++++++++++++++++++++++++++++++--
 catalog/hive/conformance_test.go   |   1 +
 catalog/rest/conformance_test.go   |   1 +
 catalog/sql/conformance_test.go    |   1 +
 5 files changed, 202 insertions(+), 6 deletions(-)

diff --git a/catalog/catalogtest/catalogtest.go b/catalog/catalogtest/catalogtest.go
index f2299add8..0521f24a2 100644
--- a/catalog/catalogtest/catalogtest.go
+++ b/catalog/catalogtest/catalogtest.go
@@ -82,6 +82,13 @@ type Config struct {
 	// once per test; implementations should register any teardown with
 	// t.Cleanup rather than relying on the suite to release resources.
 	NewCatalog func(t *testing.T) catalog.Catalog
+
+	// SupportsNamespaceProperties reports whether the catalog can store
+	// properties against a namespace. Tests that write properties are skipped
+	// when it is false, as they are for the Hadoop catalog, whose namespaces
+	// are directories with nowhere to keep them. Loading is required of every
+	// catalog and is tested either way.
+	SupportsNamespaceProperties bool
 }
 
 // RunCatalogTests runs the conformance suite against the catalog described by
@@ -101,6 +108,15 @@ func RunCatalogTests(t *testing.T, cfg Config) {
 	t.Run("DropMissingNamespace", func(t *testing.T) { testDropMissingNamespace(t, cfg) })
 	t.Run("DropNamespaceNotEmpty", func(t *testing.T) { testDropNamespaceNotEmpty(t, cfg) })
 	t.Run("ListNamespaces", func(t *testing.T) { testListNamespaces(t, cfg) })
+
+	t.Run("CreateNamespaceWithProperties", func(t *testing.T) { testCreateNamespaceWithProperties(t, cfg) })
+	t.Run("LoadNamespaceProperties", func(t *testing.T) { testLoadNamespaceProperties(t, cfg) })
+	t.Run("SetNamespaceProperties", func(t *testing.T) { testSetNamespaceProperties(t, cfg) })
+	t.Run("UpdateNamespaceProperties", func(t *testing.T) { testUpdateNamespaceProperties(t, cfg) })
+	t.Run("UpdateAndSetNamespaceProperties", func(t *testing.T) { testUpdateAndSetNamespaceProperties(t, cfg) })
+	t.Run("RemoveNamespaceProperties", func(t *testing.T) { testRemoveNamespaceProperties(t, cfg) })
+	t.Run("SetNamespacePropertiesNamespaceDoesNotExist", func(t *testing.T) { testSetNamespacePropertiesNamespaceDoesNotExist(t, cfg) })
+	t.Run("RemoveNamespacePropertiesNamespaceDoesNotExist", func(t *testing.T) { testRemoveNamespacePropertiesNamespaceDoesNotExist(t, cfg) })
 }
 
 // testBasicCreateTable asserts that a newly created table is visible to the
diff --git a/catalog/catalogtest/namespaces.go b/catalog/catalogtest/namespaces.go
index bc215c6d9..c4cbcc315 100644
--- a/catalog/catalogtest/namespaces.go
+++ b/catalog/catalogtest/namespaces.go
@@ -17,17 +17,21 @@
 
 package catalogtest
 
-// This file covers the structural namespace lifecycle: create, exists, list,
-// and drop. Namespace *properties* conformance — CreateNamespace with non-nil
-// properties, LoadNamespaceProperties, and UpdateNamespaceProperties — is
-// deferred to a later slice of #1691, together with the Config capability flags
-// (e.g. SupportsNamespaceProperties, SupportsNestedNamespaces) that gate tests
-// for backends which do not support those features.
+// This file covers namespaces: the structural lifecycle (create, exists, list,
+// drop) and properties (create with properties, load, set, update, remove).
+// Nested namespaces are deferred to a later slice of #1691, together with the
+// Config.SupportsNestedNamespaces flag that would gate them.
+//
+// Java splits property writes across SupportsNamespaces.setProperties and
+// removeProperties; the Go catalog has a single UpdateNamespaceProperties
+// taking both, so the tests below drive the same behaviors through it and also
+// check the PropertiesUpdateSummary it reports.
 
 import (
 	"context"
 	"testing"
 
+	"github.com/apache/iceberg-go"
 	"github.com/apache/iceberg-go/catalog"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -163,3 +167,176 @@ func testListNamespaces(t *testing.T, cfg Config) {
 	assert.NotContains(t, namespaces, first, "dropped namespace should no longer be listed")
 	assert.Contains(t, namespaces, second, "remaining namespace should still be listed")
 }
+
+// skipWithoutNamespaceProperties exempts catalogs that cannot store namespace
+// properties from the tests that write them.
+func skipWithoutNamespaceProperties(t *testing.T, cfg Config) {
+	t.Helper()
+
+	if !cfg.SupportsNamespaceProperties {
+		t.Skip("catalog does not support namespace properties")
+	}
+}
+
+// testCreateNamespaceWithProperties asserts that properties passed to
+// CreateNamespace are readable afterwards. A catalog may add properties of its
+// own, so only the created one is checked.
+func testCreateNamespaceWithProperties(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	require.NoError(t, cat.CreateNamespace(ctx, namespace, iceberg.Properties{"prop": "val"}))
+	t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+	props, err := cat.LoadNamespaceProperties(ctx, namespace)
+	require.NoError(t, err)
+	assert.Equal(t, "val", props["prop"], "create properties should be readable after create")
+}
+
+// testLoadNamespaceProperties asserts that properties can be loaded for a
+// namespace that exists and that loading a missing namespace is an error. A
+// catalog is free to return whatever properties it likes for a namespace
+// created without any, so nothing is asserted about their contents.
+func testLoadNamespaceProperties(t *testing.T, cfg Config) {
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	_, err := cat.LoadNamespaceProperties(ctx, namespace)
+	assert.ErrorIs(t, err, catalog.ErrNoSuchNamespace)
+
+	require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+	t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+	props, err := cat.LoadNamespaceProperties(ctx, namespace)
+	require.NoError(t, err)
+	assert.NotNil(t, props, "loaded properties should be non-nil")
+}
+
+// testSetNamespaceProperties asserts that properties set on an existing
+// namespace are reported as updated and are readable afterwards.
+func testSetNamespaceProperties(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+	t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+	updates := iceberg.Properties{"owner": "user", "created-at": "sometime"}
+	summary, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, updates)
+	require.NoError(t, err)
+	assert.ElementsMatch(t, []string{"owner", "created-at"}, summary.Updated, "both properties should be reported as updated")
+	assert.Empty(t, summary.Removed, "nothing was removed")
+
+	props, err := cat.LoadNamespaceProperties(ctx, namespace)
+	require.NoError(t, err)
+	assert.Subset(t, props, updates, "set properties should be readable")
+}
+
+// testUpdateNamespaceProperties asserts that setting a property that already
+// has a value overwrites it.
+func testUpdateNamespaceProperties(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+	t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+	_, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, iceberg.Properties{"owner": "user"})
+	require.NoError(t, err)
+
+	summary, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, iceberg.Properties{"owner": "newuser"})
+	require.NoError(t, err)
+	assert.Equal(t, []string{"owner"}, summary.Updated, "overwritten property should be reported as updated")
+
+	props, err := cat.LoadNamespaceProperties(ctx, namespace)
+	require.NoError(t, err)
+	assert.Equal(t, "newuser", props["owner"], "property should hold the new value")
+}
+
+// testUpdateAndSetNamespaceProperties asserts that a single update may both
+// overwrite an existing property and add a new one.
+func testUpdateAndSetNamespaceProperties(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+	t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+	_, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, iceberg.Properties{"owner": "user"})
+	require.NoError(t, err)
+
+	updates := iceberg.Properties{"owner": "newuser", "last-modified-at": "now"}
+	summary, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, updates)
+	require.NoError(t, err)
+	assert.ElementsMatch(t, []string{"owner", "last-modified-at"}, summary.Updated, "the overwritten and the added property should both be reported as updated")
+
+	props, err := cat.LoadNamespaceProperties(ctx, namespace)
+	require.NoError(t, err)
+	assert.Subset(t, props, updates, "updated properties should be readable")
+}
+
+// testRemoveNamespaceProperties asserts that a removed property is gone and
+// that the properties left alone survive.
+func testRemoveNamespaceProperties(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	require.NoError(t, cat.CreateNamespace(ctx, namespace, nil))
+	t.Cleanup(func() { _ = cat.DropNamespace(ctx, namespace) })
+
+	updates := iceberg.Properties{"owner": "user", "created-at": "sometime"}
+	_, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, updates)
+	require.NoError(t, err)
+
+	summary, err := cat.UpdateNamespaceProperties(ctx, namespace, []string{"created-at"}, nil)
+	require.NoError(t, err)
+	assert.Equal(t, []string{"created-at"}, summary.Removed, "removed property should be reported as removed")
+	assert.Empty(t, summary.Updated, "nothing was updated")
+
+	props, err := cat.LoadNamespaceProperties(ctx, namespace)
+	require.NoError(t, err)
+	assert.NotContains(t, props, "created-at", "removed property should be gone")
+	assert.Equal(t, "user", props["owner"], "untouched property should survive the removal")
+}
+
+// testSetNamespacePropertiesNamespaceDoesNotExist asserts that setting
+// properties on a namespace that was never created is an error.
+func testSetNamespacePropertiesNamespaceDoesNotExist(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	_, err := cat.UpdateNamespaceProperties(ctx, namespace, nil, iceberg.Properties{"test": "value"})
+	assert.ErrorIs(t, err, catalog.ErrNoSuchNamespace)
+}
+
+// testRemoveNamespacePropertiesNamespaceDoesNotExist asserts that removing
+// properties from a namespace that was never created is an error.
+func testRemoveNamespacePropertiesNamespaceDoesNotExist(t *testing.T, cfg Config) {
+	skipWithoutNamespaceProperties(t, cfg)
+
+	ctx := context.Background()
+	cat := cfg.NewCatalog(t)
+	namespace, _ := newIdentifiers()
+
+	_, err := cat.UpdateNamespaceProperties(ctx, namespace, []string{"a", "b"}, nil)
+	assert.ErrorIs(t, err, catalog.ErrNoSuchNamespace)
+}
diff --git a/catalog/hive/conformance_test.go b/catalog/hive/conformance_test.go
index ac7f36629..7aab243fe 100644
--- a/catalog/hive/conformance_test.go
+++ b/catalog/hive/conformance_test.go
@@ -30,6 +30,7 @@ import (
 
 func TestHiveCatalogConformance(t *testing.T) {
 	catalogtest.RunCatalogTests(t, catalogtest.Config{
+		SupportsNamespaceProperties: true,
 		NewCatalog: func(t *testing.T) catalog.Catalog {
 			cat, err := NewCatalog(iceberg.Properties{
 				URI:       getTestHiveURI(),
diff --git a/catalog/rest/conformance_test.go b/catalog/rest/conformance_test.go
index 5fce245c5..3e8bda67e 100644
--- a/catalog/rest/conformance_test.go
+++ b/catalog/rest/conformance_test.go
@@ -32,6 +32,7 @@ import (
 
 func TestRestCatalogConformance(t *testing.T) {
 	catalogtest.RunCatalogTests(t, catalogtest.Config{
+		SupportsNamespaceProperties: true,
 		NewCatalog: func(t *testing.T) catalog.Catalog {
 			cat, err := catalog.Load(context.Background(), "local", iceberg.Properties{
 				"type":               "rest",
diff --git a/catalog/sql/conformance_test.go b/catalog/sql/conformance_test.go
index 4abdc062f..2fca4fb89 100644
--- a/catalog/sql/conformance_test.go
+++ b/catalog/sql/conformance_test.go
@@ -32,6 +32,7 @@ import (
 
 func TestSqlCatalogConformance(t *testing.T) {
 	catalogtest.RunCatalogTests(t, catalogtest.Config{
+		SupportsNamespaceProperties: true,
 		NewCatalog: func(t *testing.T) catalog.Catalog {
 			warehouse := t.TempDir()