@@ -40,15 +40,26 @@ def _escape_athena_string_literal(value: Any) -> str:
4040
4141
4242def _escape_athena_identifier (name : Any ) -> str :
43- # Used for identifiers (column/table names) spliced inside double-quote delimited
44- # identifiers, e.g. "<name>". Column names can originate from the Glue Data Catalog
45- # during automatic schema reconciliation, so they are not necessarily caller-trusted.
43+ # For identifiers (column/table names) spliced inside DOUBLE-QUOTE delimited
44+ # identifiers in DML statements (SELECT/INSERT/MERGE/DELETE), e.g. "<name>".
45+ # Column names can originate from the Glue Data Catalog during automatic schema
46+ # reconciliation, so they are not necessarily caller-trusted.
4647 #
47- # Trino/Athena delimited identifiers escape an embedded double-quote by doubling it.
48- # Without this, a name containing " closes the identifier and appends arbitrary SQL.
48+ # Athena's Trino-based DML engine escapes an embedded double-quote in a delimited
49+ # identifier by doubling it. Without this, a name containing " closes the
50+ # identifier and the remainder is parsed as SQL.
4951 return str (name ).replace ('"' , '""' )
5052
5153
54+ def _escape_athena_ddl_identifier (name : Any ) -> str :
55+ # For identifiers spliced inside BACKTICK delimited identifiers in DDL statements
56+ # (CREATE TABLE / ALTER TABLE), e.g. `<name>`. Athena's Hive-based DDL engine uses
57+ # backticks (not double quotes) for identifier quoting and escapes an embedded
58+ # backtick by doubling it. Mixing the two families is a syntax error, so DDL and
59+ # DML splices use separate helpers.
60+ return str (name ).replace ("`" , "``" )
61+
62+
5263def _create_iceberg_table (
5364 df : pd .DataFrame ,
5465 database : str ,
@@ -73,12 +84,14 @@ def _create_iceberg_table(
7384 columns_types , _ = catalog .extract_athena_types (df = df , index = index , dtype = dtype )
7485 cols_str : str = ", " .join (
7586 [
76- f"{ k } { v } "
87+ f"` { _escape_athena_ddl_identifier ( k ) } ` { v } "
7788 if (columns_comments is None or columns_comments .get (k ) is None )
78- else f"{ k } { v } COMMENT '{ _escape_athena_string_literal (columns_comments [k ])} '"
89+ else f"` { _escape_athena_ddl_identifier ( k ) } ` { v } COMMENT '{ _escape_athena_string_literal (columns_comments [k ])} '"
7990 for k , v in columns_types .items ()
8091 ]
8192 )
93+ # partition_cols may be partition transform expressions (e.g. "day(ts)", "truncate(10, col)"),
94+ # not plain identifiers, so they are spliced verbatim rather than quoted as identifiers.
8295 partition_cols_str : str = f"PARTITIONED BY ({ ', ' .join ([col for col in partition_cols ])} )" if partition_cols else ""
8396 table_properties_str : str = (
8497 ", "
@@ -93,9 +106,9 @@ def _create_iceberg_table(
93106 )
94107
95108 create_sql : str = (
96- f"CREATE TABLE IF NOT EXISTS `{ table } ` ({ cols_str } ) "
109+ f"CREATE TABLE IF NOT EXISTS `{ _escape_athena_ddl_identifier ( table ) } ` ({ cols_str } ) "
97110 f"{ partition_cols_str } "
98- f"LOCATION '{ path } ' "
111+ f"LOCATION '{ _escape_athena_string_literal ( path ) } ' "
99112 f"TBLPROPERTIES ('table_type' ='ICEBERG', 'format'='parquet'{ table_properties_str } )"
100113 )
101114
@@ -227,10 +240,10 @@ def _alter_iceberg_table_add_columns_sql(
227240 columns_to_add : dict [str , str ],
228241) -> list [str ]:
229242 add_cols_str = ", " .join (
230- [f'" { _escape_athena_identifier (col_name )} " { columns_to_add [col_name ]} ' for col_name in columns_to_add ]
243+ [f"` { _escape_athena_ddl_identifier (col_name )} ` { columns_to_add [col_name ]} " for col_name in columns_to_add ]
231244 )
232245
233- return [f' ALTER TABLE " { _escape_athena_identifier (table )} " ADD COLUMNS ({ add_cols_str } )' ]
246+ return [f" ALTER TABLE ` { _escape_athena_ddl_identifier (table )} ` ADD COLUMNS ({ add_cols_str } )" ]
234247
235248
236249def _alter_iceberg_table_change_columns_sql (
@@ -240,9 +253,9 @@ def _alter_iceberg_table_change_columns_sql(
240253 sql_statements = []
241254
242255 for col_name , col_type in columns_to_change .items ():
243- escaped_table = _escape_athena_identifier (table )
244- escaped_col = _escape_athena_identifier (col_name )
245- sql_statements .append (f' ALTER TABLE " { escaped_table } " CHANGE COLUMN " { escaped_col } " " { escaped_col } " { col_type } ' )
256+ escaped_table = _escape_athena_ddl_identifier (table )
257+ escaped_col = _escape_athena_ddl_identifier (col_name )
258+ sql_statements .append (f" ALTER TABLE ` { escaped_table } ` CHANGE COLUMN ` { escaped_col } ` ` { escaped_col } ` { col_type } " )
246259
247260 return sql_statements
248261
@@ -666,7 +679,7 @@ def to_iceberg( # noqa: PLR0913
666679 )
667680 # if mode == "overwrite", delete whole data from table (but not table itself)
668681 elif mode == "overwrite" :
669- delete_sql_statement = f" DELETE FROM { table } "
682+ delete_sql_statement = f' DELETE FROM " { _escape_athena_identifier ( table ) } "'
670683 delete_query_execution_id : str = _start_query_execution (
671684 sql = delete_sql_statement ,
672685 workgroup = workgroup ,
0 commit comments