Repository: apache/iceberg-go
Pull request #359 (2025-03-23 09:47:26)

 feat: (catalog/glue): Add RegisterTable method in Glue catalog
Hi team,
Im adding RegisterTable implementation for Glue catalog to match what we currently have in py-iceberg and REST catalog
https://github.com/apache/iceberg-python/blob/83cc1783e20edf8998c20fed47b5f126481db4db/pyiceberg/catalog/glue.py#L443-L463


Patch:
diff --git a/README.md b/README.md
index 30ebcaa..2bfe7bb 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,7 @@ $ cd iceberg-go/cmd/iceberg && go build .
 | Load Table               |  X   |      |          |  X   |  X  |
 | List Tables              |  X   |      |          |  X   |  X  |
 | Create Table             |  X   |      |          |  X   |  X  |
+| Register Table           |  X   |      |          |  X   |     |
 | Update Current Snapshot  |  X   |      |          |      |     |
 | Create New Snapshot      |  X   |      |          |      |     |
 | Rename Table             |  X   |      |          |  X   |  X  |
diff --git a/catalog/glue/glue.go b/catalog/glue/glue.go
index 3135d53..713ae89 100644
--- a/catalog/glue/glue.go
+++ b/catalog/glue/glue.go
@@ -288,6 +288,49 @@ func (c *Catalog) CreateTable(ctx context.Context, identifier table.Identifier,
 	return createdTable, nil
 }
 
+// RegisterTable registers a new table using existing metadata.
+func (c *Catalog) RegisterTable(ctx context.Context, identifier table.Identifier, metadataLocation string) (*table.Table, error) {
+	database, tableName, err := identifierToGlueTable(identifier)
+	if err != nil {
+		return nil, err
+	}
+	// Load the metadata file to get table properties
+	ctx = utils.WithAwsConfig(ctx, c.awsCfg)
+	iofs, err := io.LoadFS(ctx, nil, metadataLocation)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load metadata file at %s: %w", metadataLocation, err)
+	}
+	// Read the metadata file
+	metadata, err := table.NewFromLocation([]string{tableName}, metadataLocation, iofs, c)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read table metadata from %s: %w", metadataLocation, err)
+	}
+	tableInput := &types.TableInput{
+		Name:       aws.String(tableName),
+		Parameters: map[string]string{},
+		TableType:  aws.String("EXTERNAL_TABLE"),
+		StorageDescriptor: &types.StorageDescriptor{
+			Location: aws.String(metadataLocation),
+			Columns:  schemaToGlueColumns(metadata.Schema()),
+		},
+	}
+	_, err = c.glueSvc.CreateTable(ctx, &glue.CreateTableInput{
+		CatalogId:    c.catalogId,
+		DatabaseName: aws.String(database),
+		TableInput:   tableInput,
+		OpenTableFormatInput: &types.OpenTableFormatInput{
+			IcebergInput: &types.IcebergInput{
+				MetadataOperation: types.MetadataOperationCreate,
+			},
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to register table %s.%s: %w", database, tableName, err)
+	}
+
+	return c.LoadTable(ctx, identifier, nil)
+}
+
 func (c *Catalog) CommitTable(context.Context, *table.Table, []table.Requirement, []table.Update) (table.Metadata, string, error) {
 	panic("commit table not implemented for Glue Catalog")
 }
diff --git a/go.mod b/go.mod
index 95cd232..3a498d2 100644
--- a/go.mod
+++ b/go.mod
@@ -46,7 +46,7 @@ require (
 	github.com/uptrace/bun/extra/bundebug v1.2.11
 	gocloud.dev v0.40.0
 	golang.org/x/sync v0.12.0
-	google.golang.org/api v0.226.0
+	google.golang.org/api v0.227.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
diff --git a/go.sum b/go.sum
index 6531e58..1f4f6b0 100644
--- a/go.sum
+++ b/go.sum
@@ -457,8 +457,8 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
 gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
 gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
-google.golang.org/api v0.226.0 h1:9A29y1XUD+YRXfnHkO66KggxHBZWg9LsTGqm7TkUvtQ=
-google.golang.org/api v0.226.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY=
+google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc=
+google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=


Test patch:
diff --git a/catalog/glue/glue_test.go b/catalog/glue/glue_test.go
index 5d2df2b..ddedf4a 100644
--- a/catalog/glue/glue_test.go
+++ b/catalog/glue/glue_test.go
@@ -938,3 +938,47 @@ func TestGlueCreateTableRollbackOnInvalidMetadata(t *testing.T) {
 	mockGlueSvc.AssertCalled(t, "DeleteTable", mock.Anything, mock.Anything, mock.Anything)
 	mockGlueSvc.AssertCalled(t, "GetTable", mock.Anything, mock.Anything, mock.Anything)
 }
+
+func TestRegisterTableMetadataNotFound(t *testing.T) {
+	assert := require.New(t)
+	mockGlueSvc := &mockGlueClient{}
+	awsCfg, err := config.LoadDefaultConfig(context.TODO())
+	assert.NoError(err)
+	cat := &Catalog{
+		glueSvc: mockGlueSvc,
+		awsCfg:  &awsCfg,
+	}
+	_, err = cat.RegisterTable(context.Background(), catalog.ToIdentifier("test_db", "test_table"), "s3://nonexistent-bucket/metadata/metadata.json")
+	assert.Error(err)
+	assert.Contains(err.Error(), "failed to read table metadata from s3://nonexistent-bucket/metadata/metadata.json")
+}
+
+func TestRegisterTableIntegration(t *testing.T) {
+	if os.Getenv("TEST_DATABASE_NAME") == "" {
+		t.Skip()
+	}
+	if os.Getenv("TEST_TABLE_LOCATION") == "" {
+		t.Skip()
+	}
+	assert := require.New(t)
+	dbName := os.Getenv("TEST_DATABASE_NAME")
+	tableName := "test_register_table_integration"
+	// Metadata location example s3://test-bucket/metadata/0000-0000-0000.metadata.json
+	metadataLocation := os.Getenv("TEST_TABLE_LOCATION")
+
+	awsCfg, err := config.LoadDefaultConfig(context.TODO(), config.WithClientLogMode(aws.LogRequest|aws.LogResponse))
+	assert.NoError(err)
+	ctlg := NewCatalog(WithAwsConfig(awsCfg))
+
+	// Drop table if it exists
+	_ = ctlg.DropTable(context.TODO(), TableIdentifier(dbName, tableName))
+
+	tbl, err := ctlg.RegisterTable(context.TODO(), TableIdentifier(dbName, tableName), metadataLocation)
+	defer func() {
+		err = ctlg.DropTable(context.TODO(), TableIdentifier(dbName, tableName))
+		assert.NoError(err)
+	}()
+	assert.NoError(err)
+	assert.Equal([]string{tableName}, tbl.Identifier())
+	assert.Equal(metadataLocation, tbl.MetadataLocation())
+}