Skip to content

Commit 5066e76

Browse files
authored
Ensure column name is backticked during alter (#861)
1 parent 6398033 commit 5066e76

File tree

10 files changed

+92
-25
lines changed

10 files changed

+92
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
- Replace array indexing with 'get' in split_part so as not to raise exception when indexing beyond bounds ([839](https://github.com/databricks/dbt-databricks/pull/839))
2525
- Set queue enabled for Python notebook jobs ([856](https://github.com/databricks/dbt-databricks/pull/856))
26+
- Ensure columns that are added get backticked ([859](https://github.com/databricks/dbt-databricks/pull/859))
2627

2728
### Under the Hood
2829

dbt/adapters/databricks/column.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
2-
from typing import ClassVar, Optional
2+
from typing import Any, ClassVar, Optional
33

4+
from dbt.adapters.databricks.utils import quote
45
from dbt.adapters.spark.column import SparkColumn
56

67

@@ -28,3 +29,16 @@ def data_type(self) -> str:
2829

2930
def __repr__(self) -> str:
3031
return "<DatabricksColumn {} ({})>".format(self.name, self.data_type)
32+
33+
@staticmethod
34+
def get_name(column: dict[str, Any]) -> str:
35+
name = column["name"]
36+
return quote(name) if column.get("quote", False) else name
37+
38+
@staticmethod
39+
def format_remove_column_list(columns: list["DatabricksColumn"]) -> str:
40+
return ", ".join([quote(c.name) for c in columns])
41+
42+
@staticmethod
43+
def format_add_column_list(columns: list["DatabricksColumn"]) -> str:
44+
return ", ".join([f"{quote(c.name)} {c.data_type}" for c in columns])

dbt/adapters/databricks/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ def handle_missing_objects(exec: Callable[[], T], default: T) -> T:
7373
if check_not_found_error(errmsg):
7474
return default
7575
raise e
76+
77+
78+
def quote(name: str) -> str:
79+
return f"`{name}`"

dbt/include/databricks/macros/adapters/columns.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,20 @@
2525

2626
{% do return(load_result('get_columns_comments_via_information_schema').table) %}
2727
{% endmacro %}
28+
29+
{% macro databricks__alter_relation_add_remove_columns(relation, add_columns, remove_columns) %}
30+
{% if remove_columns %}
31+
{% if not relation.is_delta %}
32+
{{ exceptions.raise_compiler_error('Delta format required for dropping columns from tables') }}
33+
{% endif %}
34+
{%- call statement('alter_relation_remove_columns') -%}
35+
ALTER TABLE {{ relation }} DROP COLUMNS ({{ api.Column.format_remove_column_list(remove_columns) }})
36+
{%- endcall -%}
37+
{% endif %}
38+
39+
{% if add_columns %}
40+
{%- call statement('alter_relation_add_columns') -%}
41+
ALTER TABLE {{ relation }} ADD COLUMNS ({{ api.Column.format_add_column_list(add_columns) }})
42+
{%- endcall -%}
43+
{% endif %}
44+
{% endmacro %}
Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
{% macro databricks__alter_column_comment(relation, column_dict) %}
22
{% if config.get('file_format', default='delta') in ['delta', 'hudi'] %}
3-
{% for column_name in column_dict %}
4-
{% set comment = column_dict[column_name]['description'] %}
3+
{% for column in column_dict.values() %}
4+
{% set comment = column['description'] %}
55
{% set escaped_comment = comment | replace('\'', '\\\'') %}
66
{% set comment_query %}
7-
alter table {{ relation }} change column
8-
{{ adapter.quote(column_name) if column_dict[column_name]['quote'] else column_name }}
9-
comment '{{ escaped_comment }}';
7+
alter table {{ relation }} change column {{ api.Column.get_name(column) }} comment '{{ escaped_comment }}';
108
{% endset %}
119
{% do run_query(comment_query) %}
1210
{% endfor %}
@@ -30,18 +28,3 @@
3028
{% do alter_column_comment(relation, columns_to_persist_docs) %}
3129
{% endif %}
3230
{% endmacro %}
33-
34-
{% macro get_column_comment_sql(column_name, column_dict) -%}
35-
{% if column_name in column_dict and column_dict[column_name]["description"] -%}
36-
{% set escaped_description = column_dict[column_name]["description"] | replace("'", "\\'") %}
37-
{% set column_comment_clause = "comment '" ~ escaped_description ~ "'" %}
38-
{%- endif -%}
39-
{{ adapter.quote(column_name) }} {{ column_comment_clause }}
40-
{% endmacro %}
41-
42-
{% macro get_persist_docs_column_list(model_columns, query_columns) %}
43-
{% for column_name in query_columns %}
44-
{{ get_column_comment_sql(column_name, model_columns) }}
45-
{{- ", " if not loop.last else "" }}
46-
{% endfor %}
47-
{% endmacro %}

dbt/include/databricks/macros/relations/constraints.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
{% for column_name in column_names %}
134134
{% set column = model.get('columns', {}).get(column_name) %}
135135
{% if column %}
136-
{% set quoted_name = adapter.quote(column['name']) if column['quote'] else column['name'] %}
136+
{% set quoted_name = api.Column.get_name(column) %}
137137
{% set stmt = "alter table " ~ relation ~ " change column " ~ quoted_name ~ " set not null " ~ (constraint.expression or "") ~ ";" %}
138138
{% do statements.append(stmt) %}
139139
{% else %}
@@ -154,7 +154,7 @@
154154
{% if not column %}
155155
{{ exceptions.warn('Invalid primary key column: ' ~ column_name) }}
156156
{% else %}
157-
{% set quoted_name = adapter.quote(column['name']) if column['quote'] else column['name'] %}
157+
{% set quoted_name = api.Column.get_name(column) %}
158158
{% do quoted_names.append(quoted_name) %}
159159
{% endif %}
160160
{% endfor %}
@@ -203,7 +203,7 @@
203203
{% if not column %}
204204
{{ exceptions.warn('Invalid foreign key column: ' ~ column_name) }}
205205
{% else %}
206-
{% set quoted_name = adapter.quote(column['name']) if column['quote'] else column['name'] %}
206+
{% set quoted_name = api.Column.get_name(column) %}
207207
{% do quoted_names.append(quoted_name) %}
208208
{% endif %}
209209
{% endfor %}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ dependencies = [
7676
"freezegun",
7777
"mypy",
7878
"pre-commit",
79+
"ruff",
7980
"types-requests",
8081
"debugpy",
8182
]

tests/unit/macros/relations/test_constraint_macros.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from unittest.mock import Mock
2+
13
import pytest
24

5+
from dbt.adapters.databricks.column import DatabricksColumn
36
from tests.unit.macros.base import MacroTestBase
47

58

@@ -16,6 +19,7 @@ def macro_folders_to_load(self) -> list:
1619
def modify_context(self, default_context) -> None:
1720
# Mock local_md5
1821
default_context["local_md5"] = lambda s: f"hash({s})"
22+
default_context["api"] = Mock(Column=DatabricksColumn)
1923

2024
def render_constraints(self, template, *args):
2125
return self.run_macro(template, "databricks_constraints_to_dbt", *args)

tests/unit/test_column.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from dbt.adapters.databricks.column import DatabricksColumn
24

35

@@ -24,3 +26,41 @@ def test_convert_table_stats_with_bytes_and_rows(self):
2426
"stats:rows:label": "rows",
2527
"stats:rows:value": 12345678,
2628
}
29+
30+
31+
class TestColumnStatics:
32+
@pytest.mark.parametrize(
33+
"column, expected",
34+
[
35+
({"name": "foo", "quote": True}, "`foo`"),
36+
({"name": "foo", "quote": False}, "foo"),
37+
({"name": "foo"}, "foo"),
38+
],
39+
)
40+
def test_get_name(self, column, expected):
41+
assert DatabricksColumn.get_name(column) == expected
42+
43+
@pytest.mark.parametrize(
44+
"columns, expected",
45+
[
46+
([], ""),
47+
([DatabricksColumn("foo", "string")], "`foo`"),
48+
([DatabricksColumn("foo", "string"), DatabricksColumn("bar", "int")], "`foo`, `bar`"),
49+
],
50+
)
51+
def test_format_remove_column_list(self, columns, expected):
52+
assert DatabricksColumn.format_remove_column_list(columns) == expected
53+
54+
@pytest.mark.parametrize(
55+
"columns, expected",
56+
[
57+
([], ""),
58+
([DatabricksColumn("foo", "string")], "`foo` string"),
59+
(
60+
[DatabricksColumn("foo", "string"), DatabricksColumn("bar", "int")],
61+
"`foo` string, `bar` int",
62+
),
63+
],
64+
)
65+
def test_format_add_column_list(self, columns, expected):
66+
assert DatabricksColumn.format_add_column_list(columns) == expected

tests/unit/test_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dbt.adapters.databricks.utils import redact_credentials, remove_ansi
1+
from dbt.adapters.databricks.utils import quote, redact_credentials, remove_ansi
22

33

44
class TestDatabricksUtils:
@@ -64,3 +64,6 @@ def test_remove_ansi(self):
6464
72 # how to execute python model in notebook
6565
"""
6666
assert remove_ansi(test_string) == expected_string
67+
68+
def test_quote(self):
69+
assert quote("table") == "`table`"

0 commit comments

Comments
 (0)