Skip to content

Commit 5e9d19d

Browse files
update alter_relation_add_remove_columns to quote column names (#1204)
1 parent fed0e2e commit 5e9d19d

File tree

14 files changed

+961
-151
lines changed

14 files changed

+961
-151
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: update alter_relation_add_remove_columns to quote column names
3+
time: 2025-07-16T15:48:09.903045-07:00
4+
custom:
5+
Author: colin-rogers-dbt
6+
Issue: "63"

dbt-adapters/src/dbt/include/global_project/macros/adapters/columns.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,11 @@
130130
alter {{ relation.type }} {{ relation.render() }}
131131

132132
{% for column in add_columns %}
133-
add column {{ column.name }} {{ column.data_type }}{{ ',' if not loop.last }}
133+
add column {{ column.quoted }} {{ column.data_type }}{{ ',' if not loop.last }}
134134
{% endfor %}{{ ',' if add_columns and remove_columns }}
135135

136136
{% for column in remove_columns %}
137-
drop column {{ column.name }}{{ ',' if not loop.last }}
137+
drop column {{ column.quoted }}{{ ',' if not loop.last }}
138138
{% endfor %}
139139

140140
{%- endset -%}

dbt-athena/src/dbt/adapters/athena/column.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ class AthenaColumn(Column):
1717
"TEXT": "VARCHAR",
1818
}
1919

20+
@property
21+
def quoted(self) -> str:
22+
"""Override quoted property to ensure double quotes are used for Athena column identifiers"""
23+
return f'"{self.column}"'
24+
25+
@property
26+
def quoted_hive(self) -> str:
27+
"""
28+
For some reason, sometimes we need to use double quotes, sometimes we need to use backticks.
29+
"""
30+
return f"`{self.column}`"
31+
2032
def is_iceberg(self) -> bool:
2133
return self.table_type == TableType.ICEBERG
2234

dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/column_helpers.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
alter {{ relation.type }} {{ relation.render_hive() }}
88
add columns (
99
{%- for column in add_columns -%}
10-
{{ column.name }} {{ ddl_data_type(column.data_type, table_type) }}{{ ', ' if not loop.last }}
10+
{{ column.quoted_hive }} {{ ddl_data_type(column.data_type, table_type) }}{{ ', ' if not loop.last }}
1111
{%- endfor -%}
1212
)
1313
{%- endset -%}
@@ -24,12 +24,13 @@
2424

2525
{%- for column in remove_columns -%}
2626
{% set sql -%}
27-
alter {{ relation.type }} {{ relation.render_hive() }} drop column {{ column.name }}
27+
alter {{ relation.type }} {{ relation.render_hive() }} drop column {{ column.quoted_hive }}
2828
{% endset %}
2929
{% do run_query(sql) %}
3030
{%- endfor -%}
3131
{% endmacro %}
3232

33+
3334
{% macro alter_relation_replace_columns(relation, replace_columns = none, table_type = 'hive') -%}
3435
{% if replace_columns is none %}
3536
{% set replace_columns = [] %}
@@ -39,7 +40,7 @@
3940
alter {{ relation.type }} {{ relation.render_hive() }}
4041
replace columns (
4142
{%- for column in replace_columns -%}
42-
{{ column.name }} {{ ddl_data_type(column.data_type, table_type) }}{{ ', ' if not loop.last }}
43+
{{ column.quoted_hive }} {{ ddl_data_type(column.data_type, table_type) }}{{ ', ' if not loop.last }}
4344
{%- endfor -%}
4445
)
4546
{%- endset -%}

dbt-athena/tests/functional/adapter/test_on_schema_change.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,88 @@ def test__fail(self, project, table_type):
100100

101101
new_column_names = self._column_names(project, relation_name)
102102
assert new_column_names == ["id", "name"]
103+
104+
105+
# Test fixtures for special character column names
106+
models__table_special_chars_model = """
107+
{{
108+
config(
109+
materialized='incremental',
110+
incremental_strategy='append',
111+
on_schema_change=var("on_schema_change"),
112+
table_type=var("table_type"),
113+
)
114+
}}
115+
116+
select
117+
1 as id,
118+
'keyword' as "select"
119+
{%- if is_incremental() -%}
120+
,'new data' as "field-with-dash"
121+
{%- endif -%}
122+
"""
123+
124+
125+
class TestOnSchemaChangeSpecialChars:
126+
"""Test schema changes with special character column names for Athena
127+
128+
Tests column quoting with SQL keywords and dashes in column names.
129+
"""
130+
131+
@pytest.fixture(scope="class")
132+
def models(self):
133+
models = {}
134+
for table_type in ["hive", "iceberg"]:
135+
for on_schema_change in ["sync_all_columns", "append_new_columns"]:
136+
models[f"{table_type}_special_chars_{on_schema_change}.sql"] = (
137+
models__table_special_chars_model
138+
)
139+
return models
140+
141+
def _column_names(self, project, relation_name):
142+
result = project.run_sql(f"show columns from {relation_name}", fetch="all")
143+
column_names = [row[0].strip() for row in result]
144+
return column_names
145+
146+
@pytest.mark.parametrize("table_type", ["hive", "iceberg"])
147+
def test__append_new_columns_special_chars(self, project, table_type):
148+
"""Test that columns with special characters are properly quoted when added"""
149+
relation_name = f"{table_type}_special_chars_append_new_columns"
150+
vars = {"on_schema_change": "append_new_columns", "table_type": table_type}
151+
args = ["run", "--select", relation_name, "--vars", json.dumps(vars)]
152+
153+
# First run - creates initial table with quoted column names
154+
run_dbt(args)
155+
156+
# Second run - should append new columns with special characters
157+
run_dbt(args)
158+
159+
# Verify all columns including ones with special characters exist
160+
new_column_names = self._column_names(project, relation_name)
161+
expected_columns = [
162+
"id",
163+
"select",
164+
"field-with-dash",
165+
]
166+
assert sorted(new_column_names) == sorted(expected_columns)
167+
168+
def test__sync_all_columns_special_chars(self, project):
169+
"""Test that columns with special characters are properly quoted when synced (add/remove)"""
170+
relation_name = "hive_special_chars_sync_all_columns"
171+
vars = {"on_schema_change": "sync_all_columns", "table_type": "hive"}
172+
args = ["run", "--select", relation_name, "--vars", json.dumps(vars)]
173+
174+
# First run - creates initial table
175+
run_dbt(args)
176+
177+
# Second run - should sync columns (replace all columns)
178+
run_dbt(args)
179+
180+
# Verify columns were properly synced
181+
new_column_names = self._column_names(project, relation_name)
182+
expected_columns = [
183+
"id",
184+
"select",
185+
"field-with-dash",
186+
]
187+
assert sorted(new_column_names) == sorted(expected_columns)

0 commit comments

Comments
 (0)