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/07/24 18:08:29 UTC

(iceberg-go) branch main updated: fix(glue): reject dropping non-empty namespaces (#1520)

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 dc30f413c fix(glue): reject dropping non-empty namespaces (#1520)
dc30f413c is described below

commit dc30f413cc19d22fb22d463f909fee7981d144e8
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Fri Jul 24 20:08:24 2026 +0200

    fix(glue): reject dropping non-empty namespaces (#1520)
    
    ## Summary
    
    - check a Glue database for tables before dropping its namespace
    - return `catalog.ErrNamespaceNotEmpty` when any table exists
    - avoid deleting the database when Glue cannot determine whether it is
    empty
    
    The check includes both Iceberg and non-Iceberg tables because deleting
    a Glue database makes all tables in that database inaccessible.
    
    ## Testing
    
    - `go test ./catalog/glue`
---
 catalog/glue/glue.go      | 19 +++++++++++
 catalog/glue/glue_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 97 insertions(+), 6 deletions(-)

diff --git a/catalog/glue/glue.go b/catalog/glue/glue.go
index 144e7277d..4957464de 100644
--- a/catalog/glue/glue.go
+++ b/catalog/glue/glue.go
@@ -685,6 +685,25 @@ func (c *Catalog) DropNamespace(ctx context.Context, namespace table.Identifier)
 		return err
 	}
 
+	// Glue has no conditional database delete, so a concurrent table creation can still race this check.
+	tables, err := c.glueSvc.GetTables(ctx, &glue.GetTablesInput{
+		CatalogId:    c.catalogId,
+		DatabaseName: aws.String(databaseName),
+		MaxResults:   aws.Int32(1),
+	})
+	if err != nil {
+		return fmt.Errorf("failed to list tables in namespace %s: %w", databaseName, err)
+	}
+	if len(tables.TableList) > 0 {
+		tableType := "non-Iceberg"
+		if strings.EqualFold(tables.TableList[0].Parameters[tableParamTableType], glueTypeIceberg) {
+			tableType = "Iceberg"
+		}
+
+		return fmt.Errorf("%w: cannot drop namespace %s because it still contains %s tables",
+			catalog.ErrNamespaceNotEmpty, databaseName, tableType)
+	}
+
 	params := &glue.DeleteDatabaseInput{CatalogId: c.catalogId, Name: aws.String(databaseName)}
 	_, err = c.glueSvc.DeleteDatabase(ctx, params)
 	if err != nil {
diff --git a/catalog/glue/glue_test.go b/catalog/glue/glue_test.go
index 1cf9a5cbc..d2e0d9cd0 100644
--- a/catalog/glue/glue_test.go
+++ b/catalog/glue/glue_test.go
@@ -892,12 +892,12 @@ func TestGlueCreateNamespace(t *testing.T) {
 }
 
 func TestGlueDropNamespace(t *testing.T) {
-	assert := require.New(t)
-
 	mockGlueSvc := &mockGlueClient{}
+	catalogID := aws.String("test_catalog")
 
 	mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
-		Name: aws.String("test_namespace"),
+		CatalogId: catalogID,
+		Name:      aws.String("test_namespace"),
 	}, mock.Anything).Return(&glue.GetDatabaseOutput{
 		Database: &types.Database{
 			Name: aws.String("test_namespace"),
@@ -907,16 +907,88 @@ func TestGlueDropNamespace(t *testing.T) {
 		},
 	}, nil).Once()
 
+	mockGlueSvc.On("GetTables", mock.Anything, &glue.GetTablesInput{
+		CatalogId:    catalogID,
+		DatabaseName: aws.String("test_namespace"),
+		MaxResults:   aws.Int32(1),
+	}, mock.Anything).Return(&glue.GetTablesOutput{}, nil).Once()
+
 	mockGlueSvc.On("DeleteDatabase", mock.Anything, &glue.DeleteDatabaseInput{
-		Name: aws.String("test_namespace"),
+		CatalogId: catalogID,
+		Name:      aws.String("test_namespace"),
 	}, mock.Anything).Return(&glue.DeleteDatabaseOutput{}, nil).Once()
 
 	glueCatalog := &Catalog{
-		glueSvc: mockGlueSvc,
+		glueSvc:   mockGlueSvc,
+		catalogId: catalogID,
 	}
 
 	err := glueCatalog.DropNamespace(context.TODO(), DatabaseIdentifier("test_namespace"))
-	assert.NoError(err)
+	require.NoError(t, err)
+	mockGlueSvc.AssertExpectations(t)
+}
+
+func TestGlueDropNamespaceNotEmpty(t *testing.T) {
+	tests := []struct {
+		name            string
+		tableType       string
+		expectedMessage string
+	}{
+		{name: "iceberg table", tableType: glueTypeIceberg, expectedMessage: "still contains Iceberg tables"},
+		{name: "non-Iceberg table", tableType: glueTableType, expectedMessage: "still contains non-Iceberg tables"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockGlueSvc := &mockGlueClient{}
+
+			mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+				Name: aws.String("test_namespace"),
+			}, mock.Anything).Return(&glue.GetDatabaseOutput{
+				Database: &types.Database{Name: aws.String("test_namespace")},
+			}, nil).Once()
+			mockGlueSvc.On("GetTables", mock.Anything, &glue.GetTablesInput{
+				DatabaseName: aws.String("test_namespace"),
+				MaxResults:   aws.Int32(1),
+			}, mock.Anything).Return(&glue.GetTablesOutput{
+				TableList: []types.Table{{
+					Name:       aws.String("test_table"),
+					Parameters: map[string]string{tableParamTableType: tt.tableType},
+				}},
+			}, nil).Once()
+
+			glueCatalog := &Catalog{glueSvc: mockGlueSvc}
+			err := glueCatalog.DropNamespace(context.TODO(), DatabaseIdentifier("test_namespace"))
+
+			require.ErrorIs(t, err, catalog.ErrNamespaceNotEmpty)
+			require.ErrorContains(t, err, tt.expectedMessage)
+			mockGlueSvc.AssertNotCalled(t, "DeleteDatabase", mock.Anything, mock.Anything, mock.Anything)
+			mockGlueSvc.AssertExpectations(t)
+		})
+	}
+}
+
+func TestGlueDropNamespaceGetTablesError(t *testing.T) {
+	mockGlueSvc := &mockGlueClient{}
+	errGetTables := errors.New("get tables failed")
+
+	mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+		Name: aws.String("test_namespace"),
+	}, mock.Anything).Return(&glue.GetDatabaseOutput{
+		Database: &types.Database{Name: aws.String("test_namespace")},
+	}, nil).Once()
+	mockGlueSvc.On("GetTables", mock.Anything, &glue.GetTablesInput{
+		DatabaseName: aws.String("test_namespace"),
+		MaxResults:   aws.Int32(1),
+	}, mock.Anything).Return(&glue.GetTablesOutput{}, errGetTables).Once()
+
+	glueCatalog := &Catalog{glueSvc: mockGlueSvc}
+	err := glueCatalog.DropNamespace(context.TODO(), DatabaseIdentifier("test_namespace"))
+
+	require.ErrorIs(t, err, errGetTables)
+	require.ErrorContains(t, err, "failed to list tables in namespace test_namespace")
+	mockGlueSvc.AssertNotCalled(t, "DeleteDatabase", mock.Anything, mock.Anything, mock.Anything)
+	mockGlueSvc.AssertExpectations(t)
 }
 
 func TestGlueCheckNamespaceExists(t *testing.T) {