|
39 | 39 | _DialectArgDict = Mapping[str, Any] |
40 | 40 | CreateColumn = Any |
41 | 41 |
|
| 42 | +# Prefix of the Athena data catalog name registered for an Amazon S3 Tables |
| 43 | +# table bucket (e.g. ``s3tablescatalog/my-bucket``). It is selected via the |
| 44 | +# connection ``catalog_name``. S3 Tables are Iceberg-backed and use managed |
| 45 | +# storage, so their CREATE TABLE statements must not include a LOCATION clause. |
| 46 | +# https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrations-query-athena.html |
| 47 | +S3_TABLES_CATALOG_PREFIX = "s3tablescatalog/" |
| 48 | + |
42 | 49 |
|
43 | 50 | class AthenaTypeCompiler(GenericTypeCompiler): |
44 | 51 | """Type compiler for Amazon Athena SQL types. |
@@ -305,6 +312,10 @@ class AthenaDDLCompiler(DDLCompiler): |
305 | 312 |
|
306 | 313 | - External table creation (EXTERNAL keyword for Hive-style tables) |
307 | 314 | - Iceberg table creation (managed tables with ACID support) |
| 315 | + - Amazon S3 Tables (Iceberg-backed, managed storage): set the connection |
| 316 | + ``catalog_name`` to ``s3tablescatalog/<table-bucket>`` and use the |
| 317 | + namespace as the table ``schema``. The LOCATION clause is omitted since |
| 318 | + storage is managed. |
308 | 319 | - File formats: PARQUET, ORC, TEXTFILE, JSON, AVRO, etc. |
309 | 320 | - Row formats with SerDe specifications |
310 | 321 | - Compression settings for various file formats |
@@ -445,6 +456,74 @@ def _get_serde_properties_specification( |
445 | 456 | text.append(")") |
446 | 457 | return "\n".join(text) |
447 | 458 |
|
| 459 | + @staticmethod |
| 460 | + def _is_s3_tables_catalog(connect_opts: Mapping[str, Any]) -> bool: |
| 461 | + """Return whether the connection targets an Amazon S3 Tables catalog. |
| 462 | +
|
| 463 | + S3 Tables are queried by setting the connection ``catalog_name`` to |
| 464 | + ``s3tablescatalog/<table-bucket>`` and using the namespace as the table |
| 465 | + ``schema`` (a two-part ``namespace.table`` identifier). Athena rejects a |
| 466 | + three-part ``catalog.namespace.table`` identifier in DDL, so the catalog |
| 467 | + must be selected at the connection level. Such tables use managed |
| 468 | + storage, so their CREATE TABLE statement must omit the LOCATION clause. |
| 469 | +
|
| 470 | + Args: |
| 471 | + connect_opts: The dialect connection options. |
| 472 | +
|
| 473 | + Returns: |
| 474 | + True if ``catalog_name`` names an S3 Tables catalog. |
| 475 | + """ |
| 476 | + if not connect_opts: |
| 477 | + return False |
| 478 | + catalog = connect_opts.get("catalog_name") or "" |
| 479 | + # Athena resolves catalog names case-insensitively. |
| 480 | + return catalog.lower().startswith(S3_TABLES_CATALOG_PREFIX) |
| 481 | + |
| 482 | + def _is_iceberg_table( |
| 483 | + self, dialect_opts: _DialectArgDict, connect_opts: Mapping[str, Any] |
| 484 | + ) -> bool: |
| 485 | + """Return whether the table properties declare an Iceberg table. |
| 486 | +
|
| 487 | + Args: |
| 488 | + dialect_opts: The table's ``awsathena_*`` dialect options. |
| 489 | + connect_opts: The dialect connection options. |
| 490 | +
|
| 491 | + Returns: |
| 492 | + True if the rendered TBLPROPERTIES set ``table_type`` to Iceberg. |
| 493 | + """ |
| 494 | + table_properties = self._get_table_properties_specification( |
| 495 | + dialect_opts, connect_opts |
| 496 | + ).lower() |
| 497 | + return ("table_type" in table_properties) and ("iceberg" in table_properties) |
| 498 | + |
| 499 | + def _validate_s3_tables_create_table( |
| 500 | + self, dialect_opts: _DialectArgDict, connect_opts: Mapping[str, Any] |
| 501 | + ) -> None: |
| 502 | + """Validate a CREATE TABLE compiled against an S3 Tables catalog. |
| 503 | +
|
| 504 | + S3 Tables support only Iceberg tables on managed storage, so the table |
| 505 | + must declare ``table_type`` ICEBERG and must not specify a location. |
| 506 | + Raising here surfaces a clear client-side error instead of emitting DDL |
| 507 | + that Athena would reject. |
| 508 | +
|
| 509 | + Args: |
| 510 | + dialect_opts: The table's ``awsathena_*`` dialect options. |
| 511 | + connect_opts: The dialect connection options. |
| 512 | +
|
| 513 | + Raises: |
| 514 | + exc.CompileError: If the table is not Iceberg or specifies a location. |
| 515 | + """ |
| 516 | + if not self._is_iceberg_table(dialect_opts, connect_opts): |
| 517 | + raise exc.CompileError( |
| 518 | + "S3 Tables support only Iceberg tables; specify the dialect keyword " |
| 519 | + "argument `awsathena_tblproperties={'table_type': 'ICEBERG'}`" |
| 520 | + ) |
| 521 | + if dialect_opts["location"]: |
| 522 | + raise exc.CompileError( |
| 523 | + "S3 Tables use managed storage and do not accept a table location; " |
| 524 | + "remove the dialect keyword argument `awsathena_location`" |
| 525 | + ) |
| 526 | + |
448 | 527 | def _get_table_location( |
449 | 528 | self, table: Table, dialect_opts: _DialectArgDict, connect_opts: Mapping[str, Any] |
450 | 529 | ) -> str | None: |
@@ -662,12 +741,7 @@ def visit_create_table(self, create: CreateTable, **kwargs) -> str: |
662 | 741 | dialect = cast("AthenaDialect", self.dialect) |
663 | 742 | connect_opts = dialect._connect_options |
664 | 743 |
|
665 | | - table_properties = self._get_table_properties_specification( |
666 | | - dialect_opts, connect_opts |
667 | | - ).lower() |
668 | | - is_iceberg = False |
669 | | - if ("table_type" in table_properties) and ("iceberg" in table_properties): |
670 | | - is_iceberg = True |
| 744 | + is_iceberg = self._is_iceberg_table(dialect_opts, connect_opts) |
671 | 745 |
|
672 | 746 | # https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg-creating-tables.html |
673 | 747 | text = ["\nCREATE TABLE"] if is_iceberg else ["\nCREATE EXTERNAL TABLE"] |
@@ -705,11 +779,19 @@ def post_create_table(self, table: Table) -> str: |
705 | 779 | dialect_opts: _DialectArgDict = table.dialect_options["awsathena"] |
706 | 780 | dialect = cast("AthenaDialect", self.dialect) |
707 | 781 | connect_opts = dialect._connect_options |
708 | | - text = [ |
709 | | - self._get_row_format_specification(dialect_opts, connect_opts), |
710 | | - self._get_serde_properties_specification(dialect_opts, connect_opts), |
711 | | - self._get_file_format_specification(dialect_opts, connect_opts), |
712 | | - self._get_table_location_specification(table, dialect_opts, connect_opts), |
713 | | - self._get_table_properties_specification(dialect_opts, connect_opts), |
714 | | - ] |
| 782 | + if self._is_s3_tables_catalog(connect_opts): |
| 783 | + # S3 Tables are managed Iceberg tables: ROW FORMAT, SERDEPROPERTIES, |
| 784 | + # STORED AS, and LOCATION are not accepted, so emit only TBLPROPERTIES. |
| 785 | + self._validate_s3_tables_create_table(dialect_opts, connect_opts) |
| 786 | + text = [ |
| 787 | + self._get_table_properties_specification(dialect_opts, connect_opts), |
| 788 | + ] |
| 789 | + else: |
| 790 | + text = [ |
| 791 | + self._get_row_format_specification(dialect_opts, connect_opts), |
| 792 | + self._get_serde_properties_specification(dialect_opts, connect_opts), |
| 793 | + self._get_file_format_specification(dialect_opts, connect_opts), |
| 794 | + self._get_table_location_specification(table, dialect_opts, connect_opts), |
| 795 | + self._get_table_properties_specification(dialect_opts, connect_opts), |
| 796 | + ] |
715 | 797 | return "\n".join([t for t in text if t]) |
0 commit comments