This is an automated email from the ASF dual-hosted git repository.

laskoviymishka 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 0df6c6f31 feat(catalog/glue): support Amazon S3 Tables federated 
catalogs (#2001)
0df6c6f31 is described below

commit 0df6c6f313555e17b8c5ef7c880098427e980ffe
Author: İrem Çağın Yurttürk <[email protected]>
AuthorDate: Mon Oct 5 22:17:43 2026 +0300

    feat(catalog/glue): support Amazon S3 Tables federated catalogs (#2001)
    
    * feat(catalog/glue): support Amazon S3 Tables federated catalogs
    
    Tables in a Glue database federated to Amazon S3 Tables (ConnectionType
    aws:s3tables) could not be created through the Glue catalog: the service
    assigns storage itself, so the generic client-side location resolution
    fails with "no default path set".
    
    Detect the federated database and create such tables in two phases,
    mirroring pyiceberg: create a minimal Glue entry so S3 Tables allocates
    storage, read the assigned location, write Iceberg metadata to it, then
    repoint the Glue entry. The allocated entry is rolled back on any later
    failure. An explicit location is rejected for these tables, and a missing
    version id is treated as a failed commit.
    
    S3 Tables entries also report a service-specific Glue TableType (e.g.
    "customer"), so getRawTable now accepts any entry marked as Iceberg via
    the table_type parameter, not only EXTERNAL_TABLE.
    
    Adds unit coverage for detection, the two-phase create, location
    rejection, every rollback path, and the relaxed TableType gate, plus a
    gated live integration test.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * fix(catalog/glue): don't roll back S3 Tables table on reload failure
    
    The final LoadTable ran inside the create's rollback scope, so a
    transient read failure after a successful commit deleted an
    already-created table (and reclaimed its storage). Move the reload out
    of that scope: only allocate/write/commit failures roll back now, which
    also matches the generic create path. Document that S3 Tables keeps its
    own Glue TableType on the commit update.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * test(catalog/glue): reload a service TableType in the S3 Tables create 
test
    
    Have the successful create path's mocked reload return a service-specific
    TableType ("customer"), so the happy path also exercises the relaxed
    getRawTable gate rather than only EXTERNAL_TABLE.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * address review: lazy federation detection and scope getRawTable relaxation
    
    - CreateTable now tries the generic path first and only probes for S3 Tables
      federation when location resolution fails with ErrNoDefaultLocation. This
      removes the redundant GetDatabase on default creates and avoids a new
      glue:GetDatabase requirement on explicit-location creates.
    - Export internal.ErrNoDefaultLocation so the probe matches it via errors.Is
      instead of the error string.
    - Scope the relaxed getRawTable TableType gate to entries actually federated
      to S3 Tables (Table.FederatedTable.ConnectionType == aws:s3tables), so
      non-federated databases keep their pre-existing behavior.
    - Tests: prove explicit-location creates skip the probe, prove a 
non-federated
      iceberg-param table is still rejected, and update federated mocks.
    
    * address review: reject explicit S3 Tables location, soften federation 
probe
    
    - CreateTable rejects an explicit location for a federated S3 Tables
      database instead of silently creating a table outside managed storage.
    - isS3TablesDatabase treats AccessDeniedException as not-federated so a
      caller lacking glue:GetDatabase still reaches the generic create path.
    - Document the two residual create failures left to the service.
    - Tighten the metadata-write-failure test to assert the write error.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * catalog/glue: best-effort clean up S3 Tables metadata on commit failure
    
    When UpdateTable fails after WriteMetadata, the rollback now best-effort
    deletes the written metadata object before dropping the minimal Glue entry.
    For S3 Tables the managed location may be unreachable, so the removal error
    is ignored and reclaiming any remainder is left to the service, as 
documented.
    The existing UpdateTable-failure test now asserts the metadata file is gone.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * catalog/glue: make stranded S3 Tables entry droppable and harden the 
commit
    
    - DropTable/getRawTable now accept a minimal federated S3 Tables entry
      (format=ICEBERG, no table_type), so a double-failure leftover is cleaned
      up through DropTable, restoring parity with pyiceberg's delete_table.
    - The UpdateTable repoint preserves the service-assigned TableType instead
      of forcing EXTERNAL_TABLE, which S3 Tables rejects on write; a unit test
      now asserts the TableType we send.
    - The rollback DeleteTable runs on a context detached from a cancelled
      create (context.WithoutCancel + timeout) and joins errors with 
errors.Join.
    - isS3TablesDatabase guards against a nil Database from getDatabase.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * catalog/glue: keep the live-proven EXTERNAL_TABLE on the S3 Tables repoint
    
    Sending the allocated entry's own TableType was untested against S3 Tables
    and races the service's federation (nil right after CreateTable, "customer"
    once federated). Live testing confirmed S3 Tables accepts EXTERNAL_TABLE on
    the repoint, so send it unconditionally and assert it in the unit test.
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * catalog/glue: note S3 Tables ignores the repoint TableType (verified live)
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
    
    * fix(catalog/glue): write table metadata with the catalog AWS config
    
    The metadata writes in CreateTable, commitS3TablesTable, and CommitTable 
used
    a bare context, so the S3 FileIO resolver fell back to LoadDefaultConfig. A
    catalog built with WithAwsConfig or glue.* static credentials would then 
call
    Glue as the configured principal but PUT metadata as the ambient one (or 
fail
    despite valid configured credentials). Wrap the context with the catalog's 
AWS
    config before each write, matching RegisterTable and convertGlueToIceberg; 
for
    commitS3TablesTable this also covers the staged.FS metadata cleanup.
    
    * test(catalog/glue): mock GetDatabase in TestGlueCreateTableAlreadyExists
    
    Merging main brought in TestGlueCreateTableAlreadyExists, which does an
    explicit-location create. CreateTable now consults the database up front to
    reject federated S3 Tables locations, so the test must mock GetDatabase; a
    non-federated result lets it take the generic path and assert the
    AlreadyExists mapping. In real AWS a missing glue:GetDatabase surfaces as
    AccessDenied, which isS3TablesDatabase already treats as non-federated.
    
    * address review: resolve federation up front and harden S3 Tables create
    
    - CreateTable now fetches the database once and decides S3 Tables 
federation for
      both default and explicit-location creates, reusing the result for 
namespace
      properties so staging issues no second GetDatabase. This also catches 
catalogs
      with a `warehouse` property, where the previous lazy probe let a federated
      create write outside the managed storage.
    - Map AlreadyExistsException from the S3 Tables allocate step to
      catalog.ErrTableAlreadyExists, matching the other create paths.
    - Reject RenameTable for federated S3 Tables tables, whose managed storage 
the
      copy-then-delete rename cannot safely repoint.
    - Replace isS3TablesDatabase with lookupDatabase (AccessDenied/NotFound 
tolerant)
      and extract namespacePropsFromDatabase.
    
    Tests: credential-boundary regression tests (memfs FileIO recording the ctx 
AWS
    config) for CreateTable, commitS3TablesTable incl. the fs.Remove cleanup, 
and
    CommitTable; warehouse-set federated create; allocate AlreadyExists; rename
    rejection; catalog-id pinning; and AssertExpectations on the fall-through 
test.
    
    * Trim comments
    
    * Address review nits
    
    ---------
    
    Signed-off-by: iremcaginyurtturk <[email protected]>
---
 catalog/glue/glue.go      | 226 +++++++++++-
 catalog/glue/glue_test.go | 875 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 1092 insertions(+), 9 deletions(-)

diff --git a/catalog/glue/glue.go b/catalog/glue/glue.go
index db4fe7481..4f085b34d 100644
--- a/catalog/glue/glue.go
+++ b/catalog/glue/glue.go
@@ -41,6 +41,7 @@ import (
        "github.com/aws/aws-sdk-go-v2/credentials"
        "github.com/aws/aws-sdk-go-v2/service/glue"
        "github.com/aws/aws-sdk-go-v2/service/glue/types"
+       "github.com/aws/smithy-go"
 )
 
 const (
@@ -61,6 +62,11 @@ const (
        tableParamRenameToken              = "iceberg.go.rename-token"
        glueTypeIcebergRenaming            = "ICEBERG_RENAMING"
 
+       // glueParamFormat marks the minimal entry that makes S3 Tables allocate
+       // storage; s3TablesConnectionType tags a database federated to S3 
Tables.
+       glueParamFormat        = "format"
+       s3TablesConnectionType = "aws:s3tables"
+
        // The ID of the Glue Data Catalog where the tables reside. If none is 
provided, Glue
        // automatically uses the caller's AWS account ID by default.
        // See: 
https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-databases.html
@@ -276,20 +282,49 @@ var _ catalog.Closer = (*Catalog)(nil)
 // This function will create the metadata file in S3 using the catalog and 
table properties,
 // to determine the bucket and key for the metadata location.
 func (c *Catalog) CreateTable(ctx context.Context, identifier 
table.Identifier, schema *iceberg.Schema, opts ...catalog.CreateTableOpt) 
(*table.Table, error) {
-       // The reporter is resolved once at construction (see NewCatalog), so a 
bad
-       // metrics-reporter-impl already failed there — no per-op guard is 
needed
-       // before mutating the catalog, and the trailing LoadTable reuses the 
cached
-       // reporter.
-       staged, err := internal.CreateStagedTable(ctx, c.props, 
c.LoadNamespaceProperties, identifier, schema, opts...)
+       // A missing namespace is reported before touching Glue, matching the 
contract
+       // callers rely on (an identifier without a database is not a missing 
table).
+       if len(identifier) < 2 {
+               return nil, fmt.Errorf("%w: missing namespace or invalid 
identifier %v", catalog.ErrNoSuchNamespace, identifier)
+       }
+
+       database, tableName, err := identifierToGlueTable(identifier)
        if err != nil {
                return nil, err
        }
 
-       database, tableName, err := identifierToGlueTable(identifier)
+       // Resolve federation up front so default, explicit-location and 
`warehouse`
+       // creates all route correctly; a missing DB or AccessDenied is 
non-federated.
+       db, err := c.lookupDatabase(ctx, database)
        if err != nil {
                return nil, err
        }
 
+       if db != nil && isS3TablesFederatedDatabase(db) {
+               var cfg catalog.CreateTableCfg
+               for _, opt := range opts {
+                       opt(&cfg)
+               }
+
+               // S3 Tables assigns storage itself, so reject an explicit 
location (as pyiceberg does).
+               if cfg.Location != "" {
+                       return nil, fmt.Errorf("cannot specify a location for 
table %s.%s: S3 Tables manages storage automatically", database, tableName)
+               }
+
+               return c.createS3TablesTable(ctx, database, tableName, 
identifier, schema, opts...)
+       }
+
+       // The reporter is resolved once at construction (see NewCatalog), so a 
bad
+       // metrics-reporter-impl already failed there — no per-op guard is 
needed
+       // before mutating the catalog, and the trailing LoadTable reuses the 
cached
+       // reporter.
+       staged, err := internal.CreateStagedTable(ctx, c.props, 
c.namespacePropsFn(db), identifier, schema, opts...)
+       if err != nil {
+               return nil, err
+       }
+
+       // Use the catalog's AWS config, not the ambient chain, for the 
metadata write.
+       ctx = utils.WithAwsConfig(ctx, c.awsCfg)
        if err := internal.WriteMetadata(ctx, staged.Table); err != nil {
                return nil, err
        }
@@ -310,6 +345,160 @@ func (c *Catalog) CreateTable(ctx context.Context, 
identifier table.Identifier,
        return c.LoadTable(ctx, identifier)
 }
 
+// lookupDatabase returns (nil, nil) for a missing database or AccessDenied so
+// create paths proceed as non-federated; other errors are returned.
+func (c *Catalog) lookupDatabase(ctx context.Context, database string) 
(*types.Database, error) {
+       db, err := c.getDatabase(ctx, database)
+       if err != nil {
+               var apiErr smithy.APIError
+               if errors.Is(err, catalog.ErrNoSuchNamespace) ||
+                       (errors.As(err, &apiErr) && apiErr.ErrorCode() == 
"AccessDeniedException") {
+                       return nil, nil
+               }
+
+               return nil, err
+       }
+
+       return db, nil
+}
+
+// namespacePropsFn reuses an already-fetched database to avoid a second
+// GetDatabase, falling back to a fresh lookup when it is nil.
+func (c *Catalog) namespacePropsFn(db *types.Database) 
internal.GetNamespacePropsFn {
+       return func(ctx context.Context, namespace table.Identifier) 
(iceberg.Properties, error) {
+               if db != nil {
+                       return namespacePropsFromDatabase(db), nil
+               }
+
+               return c.LoadNamespaceProperties(ctx, namespace)
+       }
+}
+
+func isS3TablesFederatedDatabase(db *types.Database) bool {
+       return db.FederatedDatabase != nil &&
+               
strings.EqualFold(aws.ToString(db.FederatedDatabase.ConnectionType), 
s3TablesConnectionType)
+}
+
+func isS3TablesFederatedTable(tbl *types.Table) bool {
+       return tbl.FederatedTable != nil &&
+               
strings.EqualFold(aws.ToString(tbl.FederatedTable.ConnectionType), 
s3TablesConnectionType)
+}
+
+// isS3TablesIcebergEntry also accepts the minimal format=ICEBERG entry, so
+// DropTable can clean up after a failed create.
+func isS3TablesIcebergEntry(tbl *types.Table) bool {
+       if !isS3TablesFederatedTable(tbl) {
+               return false
+       }
+       tableType := tbl.Parameters[tableParamTableType]
+
+       return strings.EqualFold(tableType, glueTypeIceberg) ||
+               tableType == glueTypeIcebergRenaming ||
+               strings.EqualFold(tbl.Parameters[glueParamFormat], 
glueTypeIceberg)
+}
+
+// createS3TablesTable allocates storage with a minimal entry, then repoints 
it at
+// the written metadata; on commit failure the minimal entry is rolled back.
+func (c *Catalog) createS3TablesTable(ctx context.Context, database, tableName 
string, identifier table.Identifier, schema *iceberg.Schema, opts 
...catalog.CreateTableOpt) (*table.Table, error) {
+       _, err := c.glueSvc.CreateTable(ctx, &glue.CreateTableInput{
+               CatalogId:    c.catalogId,
+               DatabaseName: aws.String(database),
+               TableInput: &types.TableInput{
+                       Name:       aws.String(tableName),
+                       Parameters: map[string]string{glueParamFormat: 
glueTypeIceberg},
+               },
+       })
+       if err != nil {
+               if isAlreadyExistsException(err) {
+                       return nil, fmt.Errorf("failed to create table %s.%s: 
%w", database, tableName, catalog.ErrTableAlreadyExists)
+               }
+
+               return nil, fmt.Errorf("failed to allocate S3 Tables storage 
for %s.%s: %w", database, tableName, err)
+       }
+
+       if err := c.commitS3TablesTable(ctx, database, tableName, identifier, 
schema, opts...); err != nil {
+               // Roll back with a detached context so a cancelled create 
still removes
+               // the minimal entry (S3 Tables enforces per-account table 
limits).
+               cleanupCtx, cancel := 
context.WithTimeout(context.WithoutCancel(ctx), renameCleanupTimeout)
+               defer cancel()
+               if _, delErr := c.glueSvc.DeleteTable(cleanupCtx, 
&glue.DeleteTableInput{
+                       CatalogId:    c.catalogId,
+                       DatabaseName: aws.String(database),
+                       Name:         aws.String(tableName),
+               }); delErr != nil {
+                       return nil, errors.Join(err, fmt.Errorf("failed to 
clean up allocated table %s.%s: %w", database, tableName, delErr))
+               }
+
+               return nil, err
+       }
+
+       // The table is committed; load it outside the rollback scope so a 
transient
+       // read failure does not delete an already-created table.
+       return c.LoadTable(ctx, identifier)
+}
+
+// commitS3TablesTable writes metadata to the service-assigned location and 
repoints
+// the Glue entry; the caller reloads, so a read failure never triggers 
rollback.
+func (c *Catalog) commitS3TablesTable(ctx context.Context, database, tableName 
string, identifier table.Identifier, schema *iceberg.Schema, opts 
...catalog.CreateTableOpt) error {
+       // Use the catalog's AWS config for WriteMetadata and the staged.FS 
cleanup below.
+       ctx = utils.WithAwsConfig(ctx, c.awsCfg)
+
+       allocated, err := c.glueSvc.GetTable(ctx, &glue.GetTableInput{
+               CatalogId:    c.catalogId,
+               DatabaseName: aws.String(database),
+               Name:         aws.String(tableName),
+       })
+       if err != nil {
+               return fmt.Errorf("failed to load allocated S3 Tables table 
%s.%s: %w", database, tableName, err)
+       }
+       if allocated == nil || allocated.Table == nil || 
allocated.Table.StorageDescriptor == nil {
+               return fmt.Errorf("S3 Tables did not return a storage 
descriptor for %s.%s", database, tableName)
+       }
+       managedLocation := 
aws.ToString(allocated.Table.StorageDescriptor.Location)
+       if managedLocation == "" {
+               return fmt.Errorf("S3 Tables did not assign a storage location 
for %s.%s", database, tableName)
+       }
+       if allocated.Table.VersionId == nil {
+               return fmt.Errorf("cannot commit table %s.%s: because Glue 
table version id is missing", database, tableName)
+       }
+
+       // Copy rather than append onto the caller's opts, whose backing array 
may
+       // have spare capacity we would otherwise clobber.
+       stagedOpts := make([]catalog.CreateTableOpt, len(opts), len(opts)+1)
+       copy(stagedOpts, opts)
+       stagedOpts = append(stagedOpts, catalog.WithLocation(managedLocation))
+
+       staged, err := internal.CreateStagedTable(ctx, c.props, 
c.LoadNamespaceProperties, identifier, schema, stagedOpts...)
+       if err != nil {
+               return err
+       }
+
+       if err := internal.WriteMetadata(ctx, staged.Table); err != nil {
+               return err
+       }
+
+       // S3 Tables ignores TableType on this repoint (verified live) and 
keeps its own
+       // service type on read, which getRawTable tolerates.
+       _, err = c.glueSvc.UpdateTable(ctx, &glue.UpdateTableInput{
+               CatalogId:    c.catalogId,
+               DatabaseName: aws.String(database),
+               TableInput:   constructTableInput(tableName, staged.Table, 
allocated.Table),
+               VersionId:    allocated.Table.VersionId,
+               SkipArchive:  aws.Bool(c.props.GetBool(SkipArchive, 
SkipArchiveDefault)),
+       })
+       if err != nil {
+               // Best-effort: drop the metadata object we just wrote; for S3 
Tables the
+               // managed location may be unreachable, so leave any remainder 
to the service.
+               if fs, fsErr := staged.FS(ctx); fsErr == nil {
+                       _ = fs.Remove(staged.MetadataLocation())
+               }
+
+               return fmt.Errorf("failed to commit S3 Tables table %s.%s: %w", 
database, tableName, err)
+       }
+
+       return 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)
@@ -377,6 +566,8 @@ func (c *Catalog) CommitTable(ctx context.Context, 
identifier table.Identifier,
        if current != nil && staged.Metadata().Equals(current.Metadata()) {
                return current.Metadata(), current.MetadataLocation(), nil
        }
+       // Use the catalog's AWS config, not the ambient chain, for the 
metadata write.
+       ctx = utils.WithAwsConfig(ctx, c.awsCfg)
        if err := internal.WriteMetadata(ctx, staged.Table); err != nil {
                return nil, "", err
        }
@@ -436,7 +627,10 @@ func (c *Catalog) DropTable(ctx context.Context, 
identifier table.Identifier) er
                return err
        }
        tableType := glueTable.Parameters[tableParamTableType]
-       if !strings.EqualFold(tableType, glueTypeIceberg) && tableType != 
glueTypeIcebergRenaming {
+       isIceberg := strings.EqualFold(tableType, glueTypeIceberg) || tableType 
== glueTypeIcebergRenaming
+       // Also allow a minimal federated S3 Tables entry (format=ICEBERG, no
+       // table_type) so a failed create can be cleaned up through DropTable.
+       if !isIceberg && !isS3TablesIcebergEntry(glueTable) {
                return fmt.Errorf("table %s.%s is not an iceberg table", 
database, tableName)
        }
 
@@ -498,6 +692,11 @@ func (c *Catalog) RenameTable(ctx context.Context, from, 
to table.Identifier) (*
        if err != nil {
                return nil, fmt.Errorf("failed to fetch the table %s.%s: %w", 
fromDatabase, fromTable, err)
        }
+       // Copy-then-delete could leave the destination pointing at storage S3 
Tables
+       // reclaims when the source is dropped.
+       if isS3TablesFederatedTable(fromGlueTable) {
+               return nil, fmt.Errorf("cannot rename table %s.%s: renaming is 
not supported for S3 Tables managed tables", fromDatabase, fromTable)
+       }
        if aws.ToString(fromGlueTable.VersionId) == "" {
                return nil, fmt.Errorf("failed to rename the table %s.%s: Glue 
table version id is missing", fromDatabase, fromTable)
        }
@@ -745,6 +944,12 @@ func (c *Catalog) LoadNamespaceProperties(ctx 
context.Context, namespace table.I
                return nil, err
        }
 
+       return namespacePropsFromDatabase(database), nil
+}
+
+// namespacePropsFromDatabase converts a Glue database into namespace 
properties,
+// normalizing the description key and surfacing the database location.
+func namespacePropsFromDatabase(database *types.Database) iceberg.Properties {
        props := make(map[string]string)
        if database.Parameters != nil {
                maps.Copy(props, database.Parameters)
@@ -762,7 +967,7 @@ func (c *Catalog) LoadNamespaceProperties(ctx 
context.Context, namespace table.I
                props[PropsKeyLocation] = aws.ToString(database.LocationUri)
        }
 
-       return props, nil
+       return props
 }
 
 // UpdateNamespaceProperties updates the properties of an Iceberg namespace in 
the Glue catalog.
@@ -858,7 +1063,10 @@ func (c *Catalog) getRawTable(ctx context.Context, 
database, tableName string) (
                return nil, fmt.Errorf("failed to get table %s.%s: missing Glue 
table response", database, tableName)
        }
 
-       if aws.ToString(tblRes.Table.TableType) != glueTableType {
+       // S3 Tables entries carry a service TableType (e.g. "customer"); 
accept those
+       // only when federated and marked Iceberg, not for every database.
+       isExternalTable := aws.ToString(tblRes.Table.TableType) == glueTableType
+       if !isExternalTable && !isS3TablesIcebergEntry(tblRes.Table) {
                return nil, fmt.Errorf("table %s.%s is not an EXTERNAL_TABLE", 
database, tableName)
        }
 
diff --git a/catalog/glue/glue_test.go b/catalog/glue/glue_test.go
index 3e7222dab..5d8bb00fe 100644
--- a/catalog/glue/glue_test.go
+++ b/catalog/glue/glue_test.go
@@ -38,11 +38,13 @@ import (
        iceio "github.com/apache/iceberg-go/io"
        _ "github.com/apache/iceberg-go/io/gocloud"
        "github.com/apache/iceberg-go/table"
+       "github.com/apache/iceberg-go/utils"
        "github.com/aws/aws-sdk-go-v2/aws"
        "github.com/aws/aws-sdk-go-v2/config"
        "github.com/aws/aws-sdk-go-v2/service/glue"
        "github.com/aws/aws-sdk-go-v2/service/glue/types"
        "github.com/aws/aws-sdk-go-v2/service/s3"
+       "github.com/aws/smithy-go"
        "github.com/awsdocs/aws-doc-sdk-examples/gov2/testtools"
        "github.com/google/uuid"
        "github.com/stretchr/testify/mock"
@@ -2007,6 +2009,11 @@ func TestGlueCreateTableAlreadyExists(t *testing.T) {
        assert := require.New(t)
 
        mockGlueSvc := &mockGlueClient{}
+       // An explicit-location create consults the database to reject 
federated S3
+       // Tables up front; a non-federated database takes the generic create 
path.
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput(""), nil).Once()
        mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
                Return(&glue.CreateTableOutput{}, &types.AlreadyExistsException{
                        Message: aws.String("Table already exists"),
@@ -2034,6 +2041,9 @@ func TestGlueCreateTableRollbackOnInvalidMetadata(t 
*testing.T) {
                iceberg.NestedField{ID: 1, Name: "id", Type: 
iceberg.Int64Type{}, Required: true},
                iceberg.NestedField{ID: 2, Name: "name", Type: 
iceberg.StringType{}, Required: true},
        )
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput(""), nil)
        mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).Return(&glue.CreateTableOutput{}, nil)
        mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
                DatabaseName: aws.String("test_database"),
@@ -2607,3 +2617,868 @@ func TestTableOperationsRejectEmptyIdentifiers(t 
*testing.T) {
                require.ErrorIs(t, err, catalog.ErrNoSuchTable)
        }
 }
+
+func s3TablesTestSchema() *iceberg.Schema {
+       return iceberg.NewSchemaWithIdentifiers(1, []int{1},
+               iceberg.NestedField{ID: 1, Name: "id", Type: 
iceberg.Int64Type{}, Required: true},
+               iceberg.NestedField{ID: 2, Name: "name", Type: 
iceberg.StringType{}, Required: false},
+       )
+}
+
+func federatedDatabaseOutput(connectionType string) *glue.GetDatabaseOutput {
+       db := &types.Database{Name: aws.String("test_database")}
+       if connectionType != "" {
+               db.FederatedDatabase = &types.FederatedDatabase{ConnectionType: 
aws.String(connectionType)}
+       }
+
+       return &glue.GetDatabaseOutput{Database: db}
+}
+
+func TestGlueLookupDatabase(t *testing.T) {
+       tests := []struct {
+               name           string
+               connectionType string
+               getErr         error
+               wantNil        bool
+               wantFederated  bool
+               wantErr        bool
+       }{
+               {name: "federated to s3 tables", connectionType: 
"aws:s3tables", wantFederated: true},
+               {name: "federated case insensitive", connectionType: 
"AWS:S3Tables", wantFederated: true},
+               {name: "federated to another source", connectionType: 
"aws:redshift"},
+               {name: "not federated", connectionType: ""},
+               {name: "missing database tolerated", getErr: 
&types.EntityNotFoundException{}, wantNil: true},
+               {name: "access denied tolerated", getErr: 
&smithy.GenericAPIError{Code: "AccessDeniedException"}, wantNil: true},
+               {name: "get database error", getErr: errors.New("boom"), 
wantErr: true},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       mockGlueSvc := &mockGlueClient{}
+                       if tt.getErr != nil {
+                               mockGlueSvc.On("GetDatabase", mock.Anything, 
&glue.GetDatabaseInput{
+                                       Name: aws.String("test_database"),
+                               }, 
mock.Anything).Return((*glue.GetDatabaseOutput)(nil), tt.getErr).Once()
+                       } else {
+                               mockGlueSvc.On("GetDatabase", mock.Anything, 
&glue.GetDatabaseInput{
+                                       Name: aws.String("test_database"),
+                               }, 
mock.Anything).Return(federatedDatabaseOutput(tt.connectionType), nil).Once()
+                       }
+
+                       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: 
&aws.Config{}}
+                       db, err := cat.lookupDatabase(context.Background(), 
"test_database")
+                       if tt.wantErr {
+                               require.Error(t, err)
+                       } else {
+                               require.NoError(t, err)
+                               if tt.wantNil {
+                                       require.Nil(t, db)
+                               } else {
+                                       require.NotNil(t, db)
+                                       require.Equal(t, tt.wantFederated, 
isS3TablesFederatedDatabase(db))
+                               }
+                       }
+                       mockGlueSvc.AssertExpectations(t)
+               })
+       }
+}
+
+// TestGlueCreateTableS3TablesFederated exercises the full two-phase create.
+func TestGlueCreateTableS3TablesFederated(t *testing.T) {
+       ctx := context.Background()
+       managedLocation := "file://" + t.TempDir()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.MatchedBy(func(in 
*glue.CreateTableInput) bool {
+               return aws.ToString(in.TableInput.Name) == "test_table" &&
+                       in.TableInput.Parameters[glueParamFormat] == 
glueTypeIceberg &&
+                       in.TableInput.StorageDescriptor == nil
+       }), mock.Anything).Return(&glue.CreateTableOutput{}, nil).Once()
+
+       allocated := &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               VersionId:         aws.String("1"),
+               TableType:         aws.String("customer"),
+               Parameters:        map[string]string{glueParamFormat: 
glueTypeIceberg},
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: allocated}, 
nil).Once()
+
+       // LoadTable at the end reads this entry; UpdateTable's Run below fills 
in the
+       // iceberg parameters (including the metadata pointer) before it is 
read.
+       loaded := &types.Table{
+               Name:         aws.String("test_table"),
+               DatabaseName: aws.String("test_database"),
+               // S3 Tables reports its own service TableType plus a 
FederatedTable marker,
+               // exercising the relaxed getRawTable gate on the create 
success path.
+               TableType:         aws.String("customer"),
+               FederatedTable:    &types.FederatedTable{ConnectionType: 
aws.String(s3TablesConnectionType)},
+               Parameters:        map[string]string{},
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }
+       var capturedMetadataLocation string
+       mockGlueSvc.On("UpdateTable", mock.Anything, mock.MatchedBy(func(in 
*glue.UpdateTableInput) bool {
+               // The repoint sends EXTERNAL_TABLE even though the allocated 
entry reports
+               // the service type "customer"; live testing confirmed S3 
Tables accepts it.
+               return in.TableInput != nil && aws.ToString(in.VersionId) == 
"1" &&
+                       in.TableInput.Parameters[tableParamTableType] == 
glueTypeIceberg &&
+                       aws.ToString(in.TableInput.TableType) == glueTableType
+       }), mock.Anything).Run(func(args mock.Arguments) {
+               in := args.Get(1).(*glue.UpdateTableInput)
+               capturedMetadataLocation = 
in.TableInput.Parameters[tableParamMetadataLocation]
+               loaded.Parameters[tableParamTableType] = glueTypeIceberg
+               loaded.Parameters[tableParamMetadataLocation] = 
capturedMetadataLocation
+       }).Return(&glue.UpdateTableOutput{}, nil).Once()
+
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: loaded}, nil)
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       tbl, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.NoError(t, err)
+       require.Equal(t, TableIdentifier("test_database", "test_table"), 
tbl.Identifier())
+       require.Equal(t, schema.Fields(), tbl.Schema().Fields())
+       require.Contains(t, tbl.MetadataLocation(), managedLocation)
+       require.Equal(t, capturedMetadataLocation, tbl.MetadataLocation())
+       require.FileExists(t, strings.TrimPrefix(capturedMetadataLocation, 
"file://"))
+       mockGlueSvc.AssertNotCalled(t, "DeleteTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesCleanupOnFailure verifies the allocated entry is
+// deleted when the second phase fails, leaving no half-created table behind.
+func TestGlueCreateTableS3TablesCleanupOnFailure(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String("")},
+       }}, nil).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, &glue.DeleteTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.DeleteTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "did not assign a storage location")
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesCleanupErrorWrapped surfaces both the original
+// failure and the cleanup failure when deleting the allocated entry also 
fails.
+func TestGlueCreateTableS3TablesCleanupErrorWrapped(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Return((*glue.GetTableOutput)(nil), errors.New("get 
boom")).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return((*glue.DeleteTableOutput)(nil), errors.New("delete 
boom")).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "get boom")
+       require.ErrorContains(t, err, "failed to clean up allocated table")
+       require.ErrorContains(t, err, "delete boom")
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesAllocateError returns early without cleanup when
+// the initial allocation call itself fails.
+func TestGlueCreateTableS3TablesAllocateError(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return((*glue.CreateTableOutput)(nil), errors.New("allocate 
boom")).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "failed to allocate S3 Tables storage")
+       mockGlueSvc.AssertNotCalled(t, "GetTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertNotCalled(t, "DeleteTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesRejectsExplicitLocation verifies an explicit 
location
+// is refused for a federated database and nothing is created.
+func TestGlueCreateTableS3TablesRejectsExplicitLocation(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema,
+               catalog.WithLocation("file:///tmp/whatever"))
+       require.ErrorContains(t, err, "S3 Tables manages storage automatically")
+       mockGlueSvc.AssertNotCalled(t, "CreateTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableExplicitLocationNonFederated confirms an explicit 
location
+// on a non-federated database still takes the generic path.
+func TestGlueCreateTableExplicitLocationNonFederated(t *testing.T) {
+       ctx := context.Background()
+       location := "file://" + t.TempDir()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput(""), nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       // The trailing reload is not the point here; fail it fast to avoid a 
full
+       // metadata round-trip. What matters is that the generic create path 
runs.
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Return((*glue.GetTableOutput)(nil), errors.New("load 
boom")).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema,
+               catalog.WithLocation(location))
+       require.ErrorContains(t, err, "load boom")
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesMissingVersionId fails and rolls back when the
+// allocated entry has no Glue version id to commit against.
+func TestGlueCreateTableS3TablesMissingVersionId(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String("file:///tmp/whatever")},
+       }}, nil).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, &glue.DeleteTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.DeleteTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "Glue table version id is missing")
+       mockGlueSvc.AssertNotCalled(t, "UpdateTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableNonFederatedFallsThrough confirms a non-federated 
database
+// with no location still hits the generic path (and its "no default path" 
error).
+func TestGlueCreateTableNonFederatedFallsThrough(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput(""), nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "no default path set")
+       mockGlueSvc.AssertNotCalled(t, "CreateTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesFederatedIntegration is gated by 
TEST_S3TABLES_CATALOG_ID
+// (<account-id>:s3tablescatalog/<bucket>) and TEST_S3TABLES_DATABASE.
+func TestGlueCreateTableS3TablesFederatedIntegration(t *testing.T) {
+       catalogID := os.Getenv("TEST_S3TABLES_CATALOG_ID")
+       dbName := os.Getenv("TEST_S3TABLES_DATABASE")
+       if catalogID == "" || dbName == "" {
+               t.Skip()
+       }
+       assert := require.New(t)
+       ctx := context.Background()
+       awsCfg, err := config.LoadDefaultConfig(ctx)
+       assert.NoError(err)
+       ctlg, err := NewCatalog(WithAwsConfig(awsCfg), 
WithAwsProperties(AwsProperties{CatalogIdKey: catalogID}))
+       assert.NoError(err)
+
+       tableName := fmt.Sprintf("it_%d", time.Now().UnixNano())
+       ident := TableIdentifier(dbName, tableName)
+       schema := s3TablesTestSchema()
+
+       tbl, err := ctlg.CreateTable(ctx, ident, schema)
+       assert.NoError(err)
+       defer func() { assert.NoError(ctlg.DropTable(ctx, ident)) }()
+
+       assert.Equal(ident, tbl.Identifier())
+       assert.Equal(schema.Fields(), tbl.Schema().Fields())
+       assert.Contains(tbl.MetadataLocation(), "--table-s3", "metadata must 
land in the S3 Tables managed location")
+
+       reloaded, err := ctlg.LoadTable(ctx, ident)
+       assert.NoError(err)
+       assert.Equal(schema.Fields(), reloaded.Schema().Fields())
+       assert.Equal(tbl.MetadataLocation(), reloaded.MetadataLocation())
+}
+
+// TestGlueGetRawTableTableType covers the TableType gate: EXTERNAL_TABLE and
+// federated iceberg entries pass, anything else is rejected.
+func TestGlueGetRawTableTableType(t *testing.T) {
+       tests := []struct {
+               name      string
+               tableType string
+               params    map[string]string
+               federated bool
+               wantErr   bool
+       }{
+               {
+                       name:      "standard external table",
+                       tableType: glueTableType,
+                       params:    map[string]string{tableParamTableType: 
glueTypeIceberg},
+               },
+               {
+                       name:      "s3 tables federated iceberg",
+                       tableType: "customer",
+                       params:    map[string]string{tableParamTableType: 
glueTypeIceberg},
+                       federated: true,
+               },
+               {
+                       name:      "s3 tables federated renaming",
+                       tableType: "customer",
+                       params:    map[string]string{tableParamTableType: 
glueTypeIcebergRenaming},
+                       federated: true,
+               },
+               {
+                       name:      "non-federated iceberg param unexpected type 
rejected",
+                       tableType: "customer",
+                       params:    map[string]string{tableParamTableType: 
glueTypeIceberg},
+                       wantErr:   true,
+               },
+               {
+                       name:      "non-iceberg unexpected type rejected",
+                       tableType: "VIRTUAL_VIEW",
+                       params:    map[string]string{},
+                       wantErr:   true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       mockGlueSvc := &mockGlueClient{}
+                       glueTable := &types.Table{
+                               Name:         aws.String("test_table"),
+                               DatabaseName: aws.String("test_database"),
+                               TableType:    aws.String(tt.tableType),
+                               Parameters:   tt.params,
+                       }
+                       if tt.federated {
+                               glueTable.FederatedTable = 
&types.FederatedTable{ConnectionType: aws.String(s3TablesConnectionType)}
+                       }
+                       mockGlueSvc.On("GetTable", mock.Anything, 
&glue.GetTableInput{
+                               DatabaseName: aws.String("test_database"),
+                               Name:         aws.String("test_table"),
+                       }, mock.Anything).Return(&glue.GetTableOutput{Table: 
glueTable}, nil).Once()
+
+                       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: 
&aws.Config{}}
+                       tbl, err := cat.getRawTable(context.Background(), 
"test_database", "test_table")
+                       if tt.wantErr {
+                               require.ErrorContains(t, err, "is not an 
EXTERNAL_TABLE")
+                       } else {
+                               require.NoError(t, err)
+                               require.Equal(t, tt.tableType, 
aws.ToString(tbl.TableType))
+                       }
+                       mockGlueSvc.AssertExpectations(t)
+               })
+       }
+}
+
+// TestGlueCreateTableS3TablesRollbackOnMetadataWriteFailure verifies a failed
+// metadata write rolls back the allocated entry without issuing UpdateTable.
+func TestGlueCreateTableS3TablesRollbackOnMetadataWriteFailure(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:         aws.String("test_table"),
+               DatabaseName: aws.String("test_database"),
+               VersionId:    aws.String("1"),
+               // An unregistered IO scheme fails deterministically at 
metadata write,
+               // proving the rollback is triggered by the write and not by a 
later step.
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String("unregisteredfs://bucket/table")},
+       }}, nil).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, &glue.DeleteTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.DeleteTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "scheme not registered")
+       mockGlueSvc.AssertNotCalled(t, "UpdateTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesRollbackOnUpdateFailure verifies a rejected 
repoint
+// rolls back the allocated entry.
+func TestGlueCreateTableS3TablesRollbackOnUpdateFailure(t *testing.T) {
+       ctx := context.Background()
+       dir := t.TempDir()
+       managedLocation := "file://" + dir
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               VersionId:         aws.String("1"),
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }}, nil).Once()
+       mockGlueSvc.On("UpdateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return((*glue.UpdateTableOutput)(nil), errors.New("update 
rejected")).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, &glue.DeleteTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.DeleteTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "failed to commit S3 Tables table")
+       mockGlueSvc.AssertExpectations(t)
+
+       // The best-effort cleanup should have removed the metadata object it 
wrote.
+       leftover, _ := filepath.Glob(filepath.Join(dir, "metadata", 
"*.metadata.json"))
+       require.Empty(t, leftover)
+}
+
+// TestGlueCreateTableS3TablesNoRollbackOnLoadFailure verifies that a transient
+// failure of the final reload does not delete an already-committed table.
+func TestGlueCreateTableS3TablesNoRollbackOnLoadFailure(t *testing.T) {
+       ctx := context.Background()
+       managedLocation := "file://" + t.TempDir()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               VersionId:         aws.String("1"),
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }}, nil).Once()
+       mockGlueSvc.On("UpdateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.UpdateTableOutput{}, nil).Once()
+       // The trailing reload (LoadTable -> GetTable) fails transiently.
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return((*glue.GetTableOutput)(nil), errors.New("load 
boom")).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "load boom")
+       mockGlueSvc.AssertNotCalled(t, "DeleteTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesRollbackDetachesContext verifies rollback still 
runs
+// on a detached context when the create is cancelled.
+func TestGlueCreateTableS3TablesRollbackDetachesContext(t *testing.T) {
+       ctx, cancel := context.WithCancel(context.Background())
+       defer cancel()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(federatedDatabaseOutput("aws:s3tables"), nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       // The commit load fails and the create is cancelled at the same moment.
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Run(func(mock.Arguments) { cancel() }).
+               Return((*glue.GetTableOutput)(nil), errors.New("commit 
boom")).Once()
+       // The rollback must still see a live (detached) context, not the 
cancelled one.
+       mockGlueSvc.On("DeleteTable",
+               mock.MatchedBy(func(c context.Context) bool { return c.Err() == 
nil }),
+               mock.Anything, mock.Anything).Return(&glue.DeleteTableOutput{}, 
nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorContains(t, err, "commit boom")
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueDropTableRemovesStrandedS3TablesEntry verifies DropTable removes a
+// minimal entry (format=ICEBERG, no table_type) left by a failed create.
+func TestGlueDropTableRemovesStrandedS3TablesEntry(t *testing.T) {
+       ctx := context.Background()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:           aws.String("test_table"),
+               DatabaseName:   aws.String("test_database"),
+               TableType:      aws.String("customer"),
+               FederatedTable: &types.FederatedTable{ConnectionType: 
aws.String(s3TablesConnectionType)},
+               Parameters:     map[string]string{glueParamFormat: 
glueTypeIceberg},
+       }}, nil).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, &glue.DeleteTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("test_table"),
+       }, mock.Anything).Return(&glue.DeleteTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       require.NoError(t, cat.DropTable(ctx, TableIdentifier("test_database", 
"test_table")))
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// recordingMemFS registers an in-memory FileIO that records the context's AWS
+// config on every resolution.
+func recordingMemFS(t *testing.T, scheme string) (*iceio.MemFS, 
*[]*aws.Config) {
+       t.Helper()
+       memFS := iceio.NewMemFS()
+       recorded := new([]*aws.Config)
+       iceio.Unregister(scheme)
+       iceio.Register(scheme, func(ctx context.Context, _ *url.URL, _ 
map[string]string) (iceio.IO, error) {
+               *recorded = append(*recorded, utils.GetAwsConfig(ctx))
+
+               return memFS, nil
+       })
+       t.Cleanup(func() { iceio.Unregister(scheme) })
+
+       return memFS, recorded
+}
+
+func assertRecordedConfig(t *testing.T, want *aws.Config, recorded 
*[]*aws.Config) {
+       t.Helper()
+       require.NotEmpty(t, *recorded, "expected the FileIO to be resolved at 
least once")
+       for _, got := range *recorded {
+               require.Same(t, want, got, "metadata IO must use the catalog's 
configured AWS config")
+       }
+}
+
+// TestGlueCreateTableS3TablesAllocateAlreadyExists verifies AlreadyExists 
maps to
+// ErrTableAlreadyExists without rolling back a table this call did not create.
+func TestGlueCreateTableS3TablesAllocateAlreadyExists(t *testing.T) {
+       ctx := context.Background()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, &types.AlreadyExistsException{
+                       Message: aws.String("Table already exists"),
+               }).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.ErrorIs(t, err, catalog.ErrTableAlreadyExists)
+       mockGlueSvc.AssertNotCalled(t, "DeleteTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesWarehouseSet verifies a `warehouse` property 
does not
+// bypass federation: the create still goes through the minimal-entry allocate.
+func TestGlueCreateTableS3TablesWarehouseSet(t *testing.T) {
+       ctx := context.Background()
+       managedLocation := "file://" + t.TempDir()
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       // The minimal allocate entry carries no StorageDescriptor; a 
warehouse-located
+       // create would send one. Matching on that pins the two-phase path.
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.MatchedBy(func(in 
*glue.CreateTableInput) bool {
+               return in.TableInput.Parameters[glueParamFormat] == 
glueTypeIceberg &&
+                       in.TableInput.StorageDescriptor == nil
+       }), mock.Anything).Return(&glue.CreateTableOutput{}, nil).Once()
+
+       allocated := &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               VersionId:         aws.String("1"),
+               TableType:         aws.String("customer"),
+               Parameters:        map[string]string{glueParamFormat: 
glueTypeIceberg},
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Return(&glue.GetTableOutput{Table: allocated}, nil).Once()
+
+       loaded := &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               TableType:         aws.String("customer"),
+               FederatedTable:    &types.FederatedTable{ConnectionType: 
aws.String(s3TablesConnectionType)},
+               Parameters:        map[string]string{},
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }
+       mockGlueSvc.On("UpdateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Run(func(args mock.Arguments) {
+                       in := args.Get(1).(*glue.UpdateTableInput)
+                       loaded.Parameters[tableParamTableType] = glueTypeIceberg
+                       loaded.Parameters[tableParamMetadataLocation] = 
in.TableInput.Parameters[tableParamMetadataLocation]
+               }).Return(&glue.UpdateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Return(&glue.GetTableOutput{Table: loaded}, nil)
+
+       cat := &Catalog{
+               glueSvc: mockGlueSvc,
+               awsCfg:  &aws.Config{},
+               props:   iceberg.Properties{"warehouse": 
"file:///tmp/warehouse"},
+       }
+       tbl, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.NoError(t, err)
+       require.Contains(t, tbl.MetadataLocation(), managedLocation)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableS3TablesPassesCatalogID pins that every Glue call on the
+// two-phase create path carries the federated catalog id.
+func TestGlueCreateTableS3TablesPassesCatalogID(t *testing.T) {
+       ctx := context.Background()
+       managedLocation := "file://" + t.TempDir()
+       catalogID := "123456789012:s3tablescatalog/bucket"
+       schema := s3TablesTestSchema()
+
+       matchID := func(id *string) bool { return aws.ToString(id) == catalogID 
}
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.MatchedBy(func(context.Context) bool 
{ return true }),
+               mock.MatchedBy(func(in *glue.GetDatabaseInput) bool { return 
matchID(in.CatalogId) }),
+               mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything,
+               mock.MatchedBy(func(in *glue.CreateTableInput) bool { return 
matchID(in.CatalogId) }),
+               mock.Anything).Return(&glue.CreateTableOutput{}, nil).Once()
+
+       allocated := &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               VersionId:         aws.String("1"),
+               Parameters:        map[string]string{glueParamFormat: 
glueTypeIceberg},
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }
+       mockGlueSvc.On("GetTable", mock.Anything,
+               mock.MatchedBy(func(in *glue.GetTableInput) bool { return 
matchID(in.CatalogId) }),
+               mock.Anything).Return(&glue.GetTableOutput{Table: allocated}, 
nil).Once()
+
+       loaded := &types.Table{
+               Name:              aws.String("test_table"),
+               DatabaseName:      aws.String("test_database"),
+               TableType:         aws.String("customer"),
+               FederatedTable:    &types.FederatedTable{ConnectionType: 
aws.String(s3TablesConnectionType)},
+               Parameters:        map[string]string{},
+               StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+       }
+       mockGlueSvc.On("UpdateTable", mock.Anything,
+               mock.MatchedBy(func(in *glue.UpdateTableInput) bool { return 
matchID(in.CatalogId) }),
+               mock.Anything).Run(func(args mock.Arguments) {
+               in := args.Get(1).(*glue.UpdateTableInput)
+               loaded.Parameters[tableParamTableType] = glueTypeIceberg
+               loaded.Parameters[tableParamMetadataLocation] = 
in.TableInput.Parameters[tableParamMetadataLocation]
+       }).Return(&glue.UpdateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything,
+               mock.MatchedBy(func(in *glue.GetTableInput) bool { return 
matchID(in.CatalogId) }),
+               mock.Anything).Return(&glue.GetTableOutput{Table: loaded}, nil)
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}, catalogId: 
aws.String(catalogID)}
+       tbl, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.NoError(t, err)
+       require.Contains(t, tbl.MetadataLocation(), managedLocation)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueRenameTableS3TablesRejected verifies rename is refused for a 
federated
+// S3 Tables table, which owns its managed storage, before any write.
+func TestGlueRenameTableS3TablesRejected(t *testing.T) {
+       ctx := context.Background()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
+               Name: aws.String("test_database"),
+       }, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), 
nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
+               DatabaseName: aws.String("test_database"),
+               Name:         aws.String("from_table"),
+       }, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
+               Name:           aws.String("from_table"),
+               DatabaseName:   aws.String("test_database"),
+               VersionId:      aws.String("1"),
+               TableType:      aws.String("customer"),
+               FederatedTable: &types.FederatedTable{ConnectionType: 
aws.String(s3TablesConnectionType)},
+               Parameters:     map[string]string{tableParamTableType: 
glueTypeIceberg},
+       }}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
+       _, err := cat.RenameTable(ctx,
+               TableIdentifier("test_database", "from_table"),
+               TableIdentifier("test_database", "to_table"))
+       require.ErrorContains(t, err, "renaming is not supported for S3 Tables 
managed tables")
+       mockGlueSvc.AssertNotCalled(t, "CreateTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertNotCalled(t, "DeleteTable", mock.Anything, 
mock.Anything, mock.Anything)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCreateTableUsesCatalogAwsConfig pins that the generic create path 
writes
+// and reads metadata with the catalog's configured AWS config.
+func TestGlueCreateTableUsesCatalogAwsConfig(t *testing.T) {
+       ctx := context.Background()
+       const scheme = "gluecreatecredcfg"
+       _, recorded := recordingMemFS(t, scheme)
+       awsCfg := &aws.Config{Region: "cred-regression"}
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(federatedDatabaseOutput(""), nil).Once()
+       var metadataLoc string
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.MatchedBy(func(in 
*glue.CreateTableInput) bool {
+               metadataLoc = 
in.TableInput.Parameters[tableParamMetadataLocation]
+
+               return true
+       }), mock.Anything).Return(&glue.CreateTableOutput{}, nil).Once()
+
+       loaded := &types.Table{
+               Name:         aws.String("test_table"),
+               DatabaseName: aws.String("test_database"),
+               TableType:    aws.String(glueTableType),
+               Parameters:   map[string]string{},
+       }
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Run(func(mock.Arguments) {
+                       loaded.Parameters[tableParamTableType] = glueTypeIceberg
+                       loaded.Parameters[tableParamMetadataLocation] = 
metadataLoc
+               }).Return(&glue.GetTableOutput{Table: loaded}, nil)
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: awsCfg, props: 
iceberg.Properties{"warehouse": scheme + "://bucket"}}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.NoError(t, err)
+       assertRecordedConfig(t, awsCfg, recorded)
+}
+
+// TestGlueCommitS3TablesTableUsesCatalogAwsConfig pins the catalog AWS config 
on
+// the metadata write and the fs.Remove cleanup after a failed UpdateTable.
+func TestGlueCommitS3TablesTableUsesCatalogAwsConfig(t *testing.T) {
+       ctx := context.Background()
+       const scheme = "gluecommits3credcfg"
+       const catalogID = "123456789012:s3tablescatalog/bucket"
+       _, recorded := recordingMemFS(t, scheme)
+       managedLocation := scheme + "://bucket/test_table"
+       awsCfg := &aws.Config{Region: "cred-regression"}
+       schema := s3TablesTestSchema()
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetDatabase", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(federatedDatabaseOutput("aws:s3tables"), nil).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Return(&glue.GetTableOutput{Table: &types.Table{
+                       Name:              aws.String("test_table"),
+                       DatabaseName:      aws.String("test_database"),
+                       VersionId:         aws.String("1"),
+                       StorageDescriptor: &types.StorageDescriptor{Location: 
aws.String(managedLocation)},
+               }}, nil).Once()
+       // Fail the repoint so the fs.Remove cleanup runs; the entry is then 
rolled back.
+       mockGlueSvc.On("UpdateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return((*glue.UpdateTableOutput)(nil), errors.New("update 
boom")).Once()
+       mockGlueSvc.On("DeleteTable", mock.Anything, mock.MatchedBy(func(in 
*glue.DeleteTableInput) bool {
+               return aws.ToString(in.CatalogId) == catalogID
+       }), mock.Anything).Return(&glue.DeleteTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: awsCfg, catalogId: 
aws.String(catalogID)}
+       _, err := cat.CreateTable(ctx, TableIdentifier("test_database", 
"test_table"), schema)
+       require.Error(t, err)
+       assertRecordedConfig(t, awsCfg, recorded)
+       mockGlueSvc.AssertExpectations(t)
+}
+
+// TestGlueCommitTableUsesCatalogAwsConfig pins that CommitTable writes 
metadata
+// with the catalog's configured AWS config.
+func TestGlueCommitTableUsesCatalogAwsConfig(t *testing.T) {
+       ctx := context.Background()
+       const scheme = "gluecommitcredcfg"
+       _, recorded := recordingMemFS(t, scheme)
+       awsCfg := &aws.Config{Region: "cred-regression"}
+       ident := TableIdentifier("test_database", "test_table")
+
+       mockGlueSvc := &mockGlueClient{}
+       mockGlueSvc.On("GetTable", mock.Anything, mock.Anything, mock.Anything).
+               Return(&glue.GetTableOutput{}, 
&types.EntityNotFoundException{}).Once()
+       mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, 
mock.Anything).
+               Return(&glue.CreateTableOutput{}, nil).Once()
+
+       cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: awsCfg}
+       _, _, err := cat.CommitTable(ctx, ident, 
[]table.Requirement{table.AssertCreate()}, []table.Update{
+               table.NewSetLocationUpdate(scheme + "://bucket/test_table"),
+       })
+       require.NoError(t, err)
+       assertRecordedConfig(t, awsCfg, recorded)
+       mockGlueSvc.AssertExpectations(t)
+}

Reply via email to