1616# under the License.
1717
1818
19+ import logging
1920from typing import (
2021 TYPE_CHECKING ,
2122 Any ,
120121ICEBERG_FIELD_OPTIONAL = "iceberg.field.optional"
121122ICEBERG_FIELD_CURRENT = "iceberg.field.current"
122123
124+ logger = logging .getLogger (__name__ )
125+
123126GLUE_PROFILE_NAME = "glue.profile-name"
124127GLUE_REGION = "glue.region"
125128GLUE_ACCESS_KEY_ID = "glue.access-key-id"
@@ -417,6 +420,110 @@ def _get_glue_table(self, database_name: str, table_name: str) -> "TableTypeDef"
417420 except self .glue .exceptions .EntityNotFoundException as e :
418421 raise NoSuchTableError (f"Table does not exist: { database_name } .{ table_name } " ) from e
419422
423+ def _is_s3tables_database (self , database_name : str ) -> bool :
424+ """Check if a Glue database is federated with S3 Tables.
425+
426+ S3 Tables databases have a FederatedDatabase property with
427+ ConnectionType set to aws:s3tables.
428+
429+ Args:
430+ database_name: The name of the Glue database.
431+
432+ Returns:
433+ True if the database is an S3 Tables federated database.
434+ """
435+ try :
436+ database_response = self .glue .get_database (Name = database_name )
437+ except self .glue .exceptions .EntityNotFoundException :
438+ return False
439+ database = database_response ["Database" ]
440+ federated = database .get ("FederatedDatabase" , {})
441+ return (federated .get ("ConnectionType" ) or "" ).lower () == "aws:s3tables"
442+
443+ def _create_table_s3tables (
444+ self ,
445+ identifier : str | Identifier ,
446+ database_name : str ,
447+ table_name : str ,
448+ schema : Union [Schema , "pa.Schema" ],
449+ location : str | None ,
450+ partition_spec : PartitionSpec ,
451+ sort_order : SortOrder ,
452+ properties : Properties ,
453+ ) -> Table :
454+ """Create an Iceberg table in an S3 Tables federated database.
455+
456+ S3 Tables manages storage internally, so the table location is not known until the
457+ table is created in the service. This method:
458+ 1. Creates a minimal table entry in Glue (format=ICEBERG), which causes S3 Tables
459+ to allocate storage.
460+ 2. Retrieves the managed storage location via GetTable.
461+ 3. Writes Iceberg metadata to that location.
462+ 4. Updates the Glue table entry with the metadata pointer.
463+
464+ On failure, the table created in step 1 is deleted.
465+ """
466+ if location is not None :
467+ raise ValueError (
468+ f"Cannot specify a location for S3 Tables table { database_name } .{ table_name } . "
469+ "S3 Tables manages the storage location automatically."
470+ )
471+
472+ # Create a minimal table in Glue so S3 Tables allocates storage.
473+ self ._create_glue_table (
474+ database_name = database_name ,
475+ table_name = table_name ,
476+ table_input = {
477+ "Name" : table_name ,
478+ "Parameters" : {"format" : "ICEBERG" },
479+ },
480+ )
481+
482+ try :
483+ # Retrieve the managed storage location.
484+ glue_table = self ._get_glue_table (database_name = database_name , table_name = table_name )
485+ storage_descriptor = glue_table .get ("StorageDescriptor" , {})
486+ managed_location = storage_descriptor .get ("Location" )
487+ if not managed_location :
488+ raise ValueError (f"S3 Tables did not assign a storage location for { database_name } .{ table_name } " )
489+
490+ # Build the Iceberg metadata targeting the managed location.
491+ staged_table = self ._create_staged_table (
492+ identifier = identifier ,
493+ schema = schema ,
494+ location = managed_location ,
495+ partition_spec = partition_spec ,
496+ sort_order = sort_order ,
497+ properties = properties ,
498+ )
499+
500+ # Write metadata and update the Glue table with the metadata pointer.
501+ self ._write_metadata (staged_table .metadata , staged_table .io , staged_table .metadata_location )
502+ table_input = _construct_table_input (table_name , staged_table .metadata_location , properties , staged_table .metadata )
503+ version_id = glue_table .get ("VersionId" )
504+ if not version_id :
505+ raise CommitFailedException (
506+ f"Cannot commit { database_name } .{ table_name } because Glue table version id is missing"
507+ )
508+ self ._update_glue_table (
509+ database_name = database_name ,
510+ table_name = table_name ,
511+ table_input = table_input ,
512+ version_id = version_id ,
513+ )
514+ except Exception :
515+ # Clean up the table created in step 1.
516+ try :
517+ self .glue .delete_table (DatabaseName = database_name , Name = table_name )
518+ except Exception :
519+ logger .warning (
520+ f"Failed to clean up S3 Tables table { database_name } .{ table_name } " ,
521+ exc_info = logger .isEnabledFor (logging .DEBUG ),
522+ )
523+ raise
524+
525+ return self .load_table (identifier = identifier )
526+
420527 def create_table (
421528 self ,
422529 identifier : str | Identifier ,
@@ -433,6 +540,7 @@ def create_table(
433540 identifier: Table identifier.
434541 schema: Table's schema.
435542 location: Location for the table. Optional Argument.
543+ Must not be set for S3 Tables, which manage their own storage.
436544 partition_spec: PartitionSpec for the table.
437545 sort_order: SortOrder for the table.
438546 properties: Table properties that can be a string based dictionary.
@@ -442,9 +550,24 @@ def create_table(
442550
443551 Raises:
444552 AlreadyExistsError: If a table with the name already exists.
445- ValueError: If the identifier is invalid, or no path is given to store metadata.
553+ ValueError: If the identifier is invalid, no path is given to store metadata,
554+ or a location is specified for an S3 Tables table.
446555
447556 """
557+ database_name , table_name = self .identifier_to_database_and_table (identifier )
558+
559+ if self ._is_s3tables_database (database_name ):
560+ return self ._create_table_s3tables (
561+ identifier = identifier ,
562+ database_name = database_name ,
563+ table_name = table_name ,
564+ schema = schema ,
565+ location = location ,
566+ partition_spec = partition_spec ,
567+ sort_order = sort_order ,
568+ properties = properties ,
569+ )
570+
448571 staged_table = self ._create_staged_table (
449572 identifier = identifier ,
450573 schema = schema ,
@@ -453,7 +576,6 @@ def create_table(
453576 sort_order = sort_order ,
454577 properties = properties ,
455578 )
456- database_name , table_name = self .identifier_to_database_and_table (identifier )
457579
458580 self ._write_metadata (staged_table .metadata , staged_table .io , staged_table .metadata_location )
459581 table_input = _construct_table_input (table_name , staged_table .metadata_location , properties , staged_table .metadata )
0 commit comments