@@ -39,6 +39,16 @@ def _escape_athena_string_literal(value: Any) -> str:
3939 return str (value ).replace ("'" , "''" )
4040
4141
42+ def _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.
46+ #
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.
49+ return str (name ).replace ('"' , '""' )
50+
51+
4252def _create_iceberg_table (
4353 df : pd .DataFrame ,
4454 database : str ,
@@ -216,9 +226,11 @@ def _alter_iceberg_table_add_columns_sql(
216226 table : str ,
217227 columns_to_add : dict [str , str ],
218228) -> list [str ]:
219- add_cols_str = ", " .join ([f"{ col_name } { columns_to_add [col_name ]} " for col_name in columns_to_add ])
229+ add_cols_str = ", " .join (
230+ [f'"{ _escape_athena_identifier (col_name )} " { columns_to_add [col_name ]} ' for col_name in columns_to_add ]
231+ )
220232
221- return [f" ALTER TABLE { table } ADD COLUMNS ({ add_cols_str } )" ]
233+ return [f' ALTER TABLE " { _escape_athena_identifier ( table ) } " ADD COLUMNS ({ add_cols_str } )' ]
222234
223235
224236def _alter_iceberg_table_change_columns_sql (
@@ -228,7 +240,9 @@ def _alter_iceberg_table_change_columns_sql(
228240 sql_statements = []
229241
230242 for col_name , col_type in columns_to_change .items ():
231- sql_statements .append (f"ALTER TABLE { table } CHANGE COLUMN { col_name } { col_name } { col_type } " )
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 } ' )
232246
233247 return sql_statements
234248
@@ -290,7 +304,9 @@ def _build_order_by_clause(partition_cols: list[str] | None) -> str:
290304 if not partition_cols :
291305 return ""
292306
293- order_cols = [f'"{ _extract_column_from_partition_transform (col )} "' for col in partition_cols ]
307+ order_cols = [
308+ f'"{ _escape_athena_identifier (_extract_column_from_partition_transform (col ))} "' for col in partition_cols
309+ ]
294310 return f"ORDER BY { ', ' .join (order_cols )} "
295311
296312
@@ -359,34 +375,40 @@ def _merge_iceberg(
359375 """
360376 wg_config : _WorkGroupConfig = _get_workgroup_config (session = boto3_session , workgroup = workgroup )
361377
378+ esc_database = _escape_athena_identifier (database )
379+ esc_table = _escape_athena_identifier (table )
380+ esc_source_table = _escape_athena_identifier (source_table )
381+ esc_columns = [_escape_athena_identifier (x ) for x in df .columns ]
382+
362383 sql_statement : str
363384 if merge_cols :
385+ esc_merge_cols = [_escape_athena_identifier (x ) for x in merge_cols ]
364386 if merge_condition == "update" :
365387 match_condition = f"""WHEN MATCHED THEN
366- UPDATE SET { ", " .join ([f'"{ x } " = source."{ x } "' for x in df . columns ])} """
388+ UPDATE SET { ", " .join ([f'"{ x } " = source."{ x } "' for x in esc_columns ])} """
367389 else :
368390 match_condition = ""
369391
370392 if merge_match_nulls :
371- merge_conditions = [f'(target."{ x } " IS NOT DISTINCT FROM source."{ x } ")' for x in merge_cols ]
393+ merge_conditions = [f'(target."{ x } " IS NOT DISTINCT FROM source."{ x } ")' for x in esc_merge_cols ]
372394 else :
373- merge_conditions = [f'(target."{ x } " = source."{ x } ")' for x in merge_cols ]
395+ merge_conditions = [f'(target."{ x } " = source."{ x } ")' for x in esc_merge_cols ]
374396
375397 sql_statement = f"""
376- MERGE INTO "{ database } "."{ table } " target
377- USING "{ database } "."{ source_table } " source
398+ MERGE INTO "{ esc_database } "."{ esc_table } " target
399+ USING "{ esc_database } "."{ esc_source_table } " source
378400 ON { " AND " .join (merge_conditions )}
379401 { match_condition }
380402 WHEN NOT MATCHED THEN
381- INSERT ({ ", " .join ([f'"{ x } "' for x in df . columns ])} )
382- VALUES ({ ", " .join ([f'source."{ x } "' for x in df . columns ])} )
403+ INSERT ({ ", " .join ([f'"{ x } "' for x in esc_columns ])} )
404+ VALUES ({ ", " .join ([f'source."{ x } "' for x in esc_columns ])} )
383405 """
384406 else :
385407 order_by_clause = _build_order_by_clause (partition_cols )
386408 sql_statement = f"""
387- INSERT INTO "{ database } "."{ table } " ({ ", " .join ([f'"{ x } "' for x in df . columns ])} )
388- SELECT { ", " .join ([f'"{ x } "' for x in df . columns ])}
389- FROM "{ database } "."{ source_table } "
409+ INSERT INTO "{ esc_database } "."{ esc_table } " ({ ", " .join ([f'"{ x } "' for x in esc_columns ])} )
410+ SELECT { ", " .join ([f'"{ x } "' for x in esc_columns ])}
411+ FROM "{ esc_database } "."{ esc_source_table } "
390412 { order_by_clause }
391413 """
392414
@@ -829,10 +851,14 @@ def delete_from_iceberg_table(
829851 index = False ,
830852 )
831853
854+ esc_database = _escape_athena_identifier (database )
855+ esc_table = _escape_athena_identifier (table )
856+ esc_temp_table = _escape_athena_identifier (temp_table )
857+ esc_merge_cols = [_escape_athena_identifier (x ) for x in merge_cols ]
832858 sql_statement = f"""
833- MERGE INTO "{ database } "."{ table } " target
834- USING "{ database } "."{ temp_table } " source
835- ON { " AND " .join ([f'target."{ x } " = source."{ x } "' for x in merge_cols ])}
859+ MERGE INTO "{ esc_database } "."{ esc_table } " target
860+ USING "{ esc_database } "."{ esc_temp_table } " source
861+ ON { " AND " .join ([f'target."{ x } " = source."{ x } "' for x in esc_merge_cols ])}
836862 WHEN MATCHED THEN
837863 DELETE
838864 """
0 commit comments