|
| 1 | +import contextlib |
1 | 2 | import re |
2 | 3 | import textwrap |
3 | 4 | import uuid |
|
26 | 27 | ) |
27 | 28 | from tests.pyathena.conftest import ENV |
28 | 29 |
|
| 30 | +# Amazon S3 Tables tests need a pre-provisioned table-bucket catalog and namespace. |
| 31 | +# Skip them unless AWS_ATHENA_S3_TABLES_CATALOG / AWS_ATHENA_S3_TABLES_NAMESPACE are set. |
| 32 | +requires_s3_tables = pytest.mark.skipif( |
| 33 | + not ENV.s3tables_catalog or not ENV.s3tables_namespace, |
| 34 | + reason="AWS_ATHENA_S3_TABLES_CATALOG / AWS_ATHENA_S3_TABLES_NAMESPACE are not configured", |
| 35 | +) |
| 36 | + |
29 | 37 |
|
30 | 38 | class TestSQLAlchemyAthena: |
31 | 39 | @pytest.mark.parametrize( |
@@ -2019,6 +2027,104 @@ def test_create_iceberg_table_with_multiple_partition_transform(self, engine): |
2019 | 2027 | tblproperties = actual.dialect_options["awsathena"]["tblproperties"] |
2020 | 2028 | assert tblproperties["table_type"] == "ICEBERG" |
2021 | 2029 |
|
| 2030 | + @requires_s3_tables |
| 2031 | + def test_create_s3tables_iceberg_table(self, engine): |
| 2032 | + engine, conn = engine |
| 2033 | + # S3 Tables are addressed by a three-part identifier |
| 2034 | + # (catalog.namespace.table) and use managed storage, so the emitted DDL |
| 2035 | + # quotes the catalog/namespace/table separately and omits LOCATION. |
| 2036 | + schema = f"{ENV.s3tables_catalog}.{ENV.s3tables_namespace}" |
| 2037 | + table_name = "test_create_s3tables_iceberg_table" |
| 2038 | + table = Table( |
| 2039 | + table_name, |
| 2040 | + MetaData(schema=schema), |
| 2041 | + Column("col_1", types.String), |
| 2042 | + Column("col_2", types.Integer), |
| 2043 | + awsathena_tblproperties={"table_type": "ICEBERG"}, |
| 2044 | + ) |
| 2045 | + ddl = CreateTable(table).compile(bind=conn) |
| 2046 | + |
| 2047 | + assert str(ddl) == textwrap.dedent( |
| 2048 | + f""" |
| 2049 | + CREATE TABLE `{ENV.s3tables_catalog}`.{ENV.s3tables_namespace}.{table_name} ( |
| 2050 | + \tcol_1 STRING, |
| 2051 | + \tcol_2 INT |
| 2052 | + ) |
| 2053 | + TBLPROPERTIES ( |
| 2054 | + \t'table_type' = 'ICEBERG' |
| 2055 | + ) |
| 2056 | + """ |
| 2057 | + ) |
| 2058 | + |
| 2059 | + table.create(bind=conn) |
| 2060 | + try: |
| 2061 | + # NOTE: SQLAlchemy reflection (autoload_with) of an S3 Tables table is |
| 2062 | + # not covered here. The dialect's introspection path passes the dotted |
| 2063 | + # schema straight through as the database name and does not split the |
| 2064 | + # catalog from the namespace, so reflecting a three-part identifier is |
| 2065 | + # a separate, currently-unsupported concern. Verify creation by querying |
| 2066 | + # the table directly with its double-quoted three-part identifier. |
| 2067 | + fqtn = f'"{ENV.s3tables_catalog}"."{ENV.s3tables_namespace}"."{table_name}"' |
| 2068 | + conn.execute(text(f"SELECT col_1, col_2 FROM {fqtn} LIMIT 0")) |
| 2069 | + finally: |
| 2070 | + with contextlib.suppress(Exception): |
| 2071 | + table.drop(bind=conn) |
| 2072 | + |
| 2073 | + @requires_s3_tables |
| 2074 | + def test_create_s3tables_iceberg_table_with_partition_transform(self, engine): |
| 2075 | + engine, conn = engine |
| 2076 | + schema = f"{ENV.s3tables_catalog}.{ENV.s3tables_namespace}" |
| 2077 | + table_name = "test_create_s3tables_iceberg_table_with_partition_transform" |
| 2078 | + table = Table( |
| 2079 | + table_name, |
| 2080 | + MetaData(schema=schema), |
| 2081 | + Column("col_1", types.String), |
| 2082 | + Column("dt", types.Date, awsathena_partition=True, awsathena_partition_transform="day"), |
| 2083 | + awsathena_tblproperties={"table_type": "ICEBERG"}, |
| 2084 | + ) |
| 2085 | + ddl = CreateTable(table).compile(bind=conn) |
| 2086 | + |
| 2087 | + assert str(ddl) == textwrap.dedent( |
| 2088 | + f""" |
| 2089 | + CREATE TABLE `{ENV.s3tables_catalog}`.{ENV.s3tables_namespace}.{table_name} ( |
| 2090 | + \tcol_1 STRING, |
| 2091 | + \tdt DATE |
| 2092 | + ) |
| 2093 | + PARTITIONED BY ( |
| 2094 | + \tday(dt) |
| 2095 | + ) |
| 2096 | + TBLPROPERTIES ( |
| 2097 | + \t'table_type' = 'ICEBERG' |
| 2098 | + ) |
| 2099 | + """ |
| 2100 | + ) |
| 2101 | + |
| 2102 | + table.create(bind=conn) |
| 2103 | + with contextlib.suppress(Exception): |
| 2104 | + table.drop(bind=conn) |
| 2105 | + |
| 2106 | + @requires_s3_tables |
| 2107 | + def test_create_s3tables_table_as_select(self, engine): |
| 2108 | + engine, conn = engine |
| 2109 | + # CTAS is not modeled as a SQLAlchemy construct; exercise it as raw SQL. |
| 2110 | + # The three-part identifier uses double quotes (Presto/Trino convention). |
| 2111 | + table_name = "test_create_s3tables_table_as_select" |
| 2112 | + fqtn = f'"{ENV.s3tables_catalog}"."{ENV.s3tables_namespace}"."{table_name}"' |
| 2113 | + conn.execute(text(f"DROP TABLE IF EXISTS {fqtn}")) |
| 2114 | + try: |
| 2115 | + conn.execute( |
| 2116 | + text( |
| 2117 | + f"CREATE TABLE {fqtn} " |
| 2118 | + "WITH (table_type = 'ICEBERG') AS " |
| 2119 | + "SELECT 1 AS id, 'a' AS name" |
| 2120 | + ) |
| 2121 | + ) |
| 2122 | + rows = conn.execute(text(f"SELECT id, name FROM {fqtn}")).fetchall() |
| 2123 | + assert rows == [(1, "a")] |
| 2124 | + finally: |
| 2125 | + with contextlib.suppress(Exception): |
| 2126 | + conn.execute(text(f"DROP TABLE IF EXISTS {fqtn}")) |
| 2127 | + |
2022 | 2128 | def test_insert_from_select_cte_follows_insert_one(self, engine): |
2023 | 2129 | engine, conn = engine |
2024 | 2130 | metadata = MetaData(schema=ENV.schema) |
|
0 commit comments