Repository: dbt-athena/dbt-athena (python)

Issue:
seeds with columns starting with underscore fail
When trying to seed a csv file where any column name is prefixed with an underscore we encounter a runtime error.

Seems that [columns starting with an underscore need to be enclosed in backticks](https://docs.aws.amazon.com/athena/latest/ug/tables-databases-columns-names.html#names-that-begin-with-an-underscore) in DDL statements.

Happy to raise a PR to resolve, would it just be a case of backticking column-names [here](https://github.com/dbt-athena/dbt-athena/blob/main/dbt/include/athena/macros/materializations/seeds/helpers.sql#L67) and [here](https://github.com/dbt-athena/dbt-athena/blob/main/dbt/include/athena/macros/materializations/seeds/helpers.sql#L134)?

### How to reproduce

try and seed this:
```
__column_one,column_2
a,1
b,2
c,3
```

Error:
```
Runtime Error in seed __test_seed (seeds/test_seed/__test_seed.csv)
  FAILED: ParseException line 2:76 cannot recognize input near '__column_one' 'string' ',' in column specification
```

Pull request:
fix: implement quote seed column
# Description

<!--- Add a little description on what you plan to tackle with this PR -->
<!--- Add resolve #issue-number in case you resolve an open issue -->

resolves #507

Implements `quote` method and passes `quote_config` through in `quote_seed_column`.

To get round the issue where backticks are required in some statements when columns are prefixed with underscores, but double-quotes are needed everywhere else i've added a `quote_character` attribute which defaults to double-quote and pass backticks when required.

## Models used to test - Optional
<!--- Add here the models that you use to test the changes -->

## Checklist

- [x] You followed [contributing section](https://github.com/dbt-athena/dbt-athena#contributing)
- [x] You kept your Pull Request small and focused on a single feature or bug fix.
- [x] You added unit testing when necessary
- [ ] You added functional testing when necessary


Patch:
diff --git a/dbt/adapters/athena/impl.py b/dbt/adapters/athena/impl.py
index 49289dd..1907b75 100755
--- a/dbt/adapters/athena/impl.py
+++ b/dbt/adapters/athena/impl.py
@@ -124,6 +124,8 @@ class AthenaAdapter(SQLAdapter):
     AdapterSpecificConfigs = AthenaConfig
     Column = AthenaColumn
 
+    quote_character: str = '"'  # Presto quote character
+
     # There is no such concept as constraints in Athena
     CONSTRAINT_SUPPORT = {
         ConstraintType.check: ConstraintSupport.NOT_SUPPORTED,
@@ -397,9 +399,22 @@ class AthenaAdapter(SQLAdapter):
         if table_location := self.get_glue_table_location(relation):
             self.delete_from_s3(table_location)
 
+    def quote(self, identifier: str) -> str:
+        return f"{self.quote_character}{identifier}{self.quote_character}"
+
     @available
-    def quote_seed_column(self, column: str, quote_config: Optional[bool]) -> str:
-        return str(super().quote_seed_column(column, False))
+    def quote_seed_column(
+        self, column: str, quote_config: Optional[bool], quote_character: Optional[str] = None
+    ) -> str:
+        if quote_character:
+            old_value = self.quote_character
+            object.__setattr__(self, "quote_character", quote_character)
+            quoted_column = str(super().quote_seed_column(column, quote_config))
+            object.__setattr__(self, "quote_character", old_value)
+        else:
+            quoted_column = str(super().quote_seed_column(column, quote_config))
+
+        return quoted_column
 
     @available
     def upload_seed_to_s3(
diff --git a/dbt/include/athena/macros/materializations/seeds/helpers.sql b/dbt/include/athena/macros/materializations/seeds/helpers.sql
index a789498..f44c7c9 100644
--- a/dbt/include/athena/macros/materializations/seeds/helpers.sql
+++ b/dbt/include/athena/macros/materializations/seeds/helpers.sql
@@ -64,7 +64,7 @@
           {%- set type = column_override.get(col_name, inferred_type) -%}
           {%- set type = type if type != "string" else "varchar" -%}
           {%- set column_name = (col_name | string) -%}
-          {{ adapter.quote_seed_column(column_name, quote_seed_column) }} {{ ddl_data_type(type) }} {%- if not loop.last -%}, {% endif -%}
+          {{ adapter.quote_seed_column(column_name, quote_seed_column, "`") }} {{ ddl_data_type(type) }} {%- if not loop.last -%}, {% endif -%}
         {%- endfor -%}
     )
     location '{{ location }}'
@@ -131,7 +131,7 @@
     create external table {{ tmp_relation.render_hive() }} (
         {%- for col_name in agate_table.column_names -%}
             {%- set column_name = (col_name | string) -%}
-            {{ adapter.quote_seed_column(column_name, quote_seed_column) }} string {%- if not loop.last -%}, {% endif -%}
+            {{ adapter.quote_seed_column(column_name, quote_seed_column, "`") }} string {%- if not loop.last -%}, {% endif -%}
         {%- endfor -%}
     )
     row format serde 'org.apache.hadoop.hive.serde2.OpenCSVSerde'


Test patch:
diff --git a/tests/functional/adapter/test_quote_seed_column.py b/tests/functional/adapter/test_quote_seed_column.py
new file mode 100644
index 0000000..c0845c3
--- /dev/null
+++ b/tests/functional/adapter/test_quote_seed_column.py
@@ -0,0 +1,39 @@
+import pytest
+
+from dbt.tests.adapter.basic.files import (
+    base_materialized_var_sql,
+    base_table_sql,
+    base_view_sql,
+    schema_base_yml,
+    seeds_base_csv,
+)
+from dbt.tests.adapter.basic.test_base import BaseSimpleMaterializations
+
+seed_base_csv_underscore_column = seeds_base_csv.replace("name", "_name")
+
+quote_columns_seed_schema = """
+
+seeds:
+- name: base
+  config:
+    quote_columns: true
+
+"""
+
+
+class TestSimpleMaterializationsHive(BaseSimpleMaterializations):
+    @pytest.fixture(scope="class")
+    def models(self):
+        schema = schema_base_yml + quote_columns_seed_schema
+        return {
+            "view_model.sql": base_view_sql,
+            "table_model.sql": base_table_sql,
+            "swappable.sql": base_materialized_var_sql,
+            "schema.yml": schema,
+        }
+
+    @pytest.fixture(scope="class")
+    def seeds(self):
+        return {
+            "base.csv": seed_base_csv_underscore_column,
+        }
diff --git a/tests/unit/test_adapter.py b/tests/unit/test_adapter.py
index d1cf69e..168fa3c 100644
--- a/tests/unit/test_adapter.py
+++ b/tests/unit/test_adapter.py
@@ -600,10 +600,17 @@ class TestAthenaAdapter:
         objs = s3.list_objects_v2(Bucket=BUCKET)
         assert objs["KeyCount"] == 0
 
-    @patch("dbt.adapters.athena.impl.SQLAdapter.quote_seed_column")
-    def test_quote_seed_column(self, parent_quote_seed_column):
-        self.adapter.quote_seed_column("col", None)
-        parent_quote_seed_column.assert_called_once_with("col", False)
+    @pytest.mark.parametrize(
+        "column,quote_config,quote_character,expected",
+        [
+            pytest.param("col", False, None, "col"),
+            pytest.param("col", True, None, '"col"'),
+            pytest.param("col", False, "`", "col"),
+            pytest.param("col", True, "`", "`col`"),
+        ],
+    )
+    def test_quote_seed_column(self, column, quote_config, quote_character, expected):
+        assert self.adapter.quote_seed_column(column, quote_config, quote_character) == expected
 
     @mock_glue
     @mock_athena