Repository: dbt-labs/dbt-core

Issue:
Bug for web generated documentation 
### Describe the bug
I am facing the following issue with `dbt docs generate`
We are using Snowflake and column names with double quotation marks meaning that in the `schema.yml`  file when we specify the column name we have to do the following: `'"ColumnName"'` for tests to run.
The issues is that that then we get two columns references in the web documentation we get duplicates as you can see in the Screenshots 

### Steps To Reproduce
```
dbt docs generate 
dbt docs serve 
```
=> with config as in additional information 

### Expected behavior
Support a config in the `schema.yml` spec which would wrap a given column in quotes. That way, you could avoid specifying a column like `’“ColumnName”’`, and instead just specify `ColumnName `with a `quote: true` flag provided. The docs should handle this config gracefully and it’ll be less typing/weirdness in the schema.yml file

=> suggestion from @drewbanin 


### Screenshots and log output
If applicable, add screenshots or log output to help explain your problem.
![image](https://user-images.githubusercontent.com/8689511/72348084-dc4c1c00-36d9-11ea-8bc7-cc3258b57af1.png)

### System information
**Which database are you using dbt with?**
- [ ] postgres
- [ ] redshift
- [ ] bigquery
- [x] snowflake
- [ ] other (specify: ____________)


**The output of `dbt --version`:**
```Running with dbt=0.15.0
dbt version: 0.15.0
python version: 3.7.5
os info: Windows-10-10.0.18362-SP0
```

### Additional information
the `schema.yml` :
```
models:
  - name: Majority_DimUser
    columns:
      - name: '"UserKey"'
        description: UserKey is one of the unique identifier for the User
        tests:
          - not_null
          - unique
```

the `dbt_project.yml`

```
name: 'looker_wh'
version: '1.0'

source-paths: ["models"]
analysis-paths: ["analysis"]
test-paths: ["tests"]
data-paths: ["data"]
macro-paths: ["macros"]

target-path: "target"  # directory which will store compiled SQL files
clean-targets:         # directories to be removed by `dbt clean`
    - "target"
    - "dbt_modules"

quoting:
  identifier: true
  schema: true
```


Discussion:
Thanks for the report @bcg0006! I just queued this issue up for our 0.16.0 release.

I think a viable syntax might look like:
```
models:
  - name: Majority_DimUser
    columns:
      - name: UserKey
        description: UserKey is one of the unique identifier for the User
        quote: true # This would be new
        tests:
          - not_null
          - unique
```

If dbt encountered a `true` value for the `quote` config, then dbt would quote the column before interpolating it into SQL queries (like in tests). dbt would _not_ quote the column when rendering the documentation website. I suppose it would also be good to support this config at the model-level so that you wouldn't need to quote every mixed-case column explicitly.

The other broad approach we could take here would be to fetch the columns in the destination table in a `dbt test`, then determine the appropriate quoting/casing for the column. I think that's a pretty heavy-handed way to go about doing this, but I did want to raise it as a possible solution.

Resolving patch:
diff --git a/core/dbt/adapters/base/relation.py b/core/dbt/adapters/base/relation.py
--- a/core/dbt/adapters/base/relation.py
+++ b/core/dbt/adapters/base/relation.py
@@ -288,9 +288,11 @@ def quoted(self, identifier):
     def create_from_source(
         cls: Type[Self], source: ParsedSourceDefinition, **kwargs: Any
     ) -> Self:
+        source_quoting = source.quoting.to_dict()
+        source_quoting.pop('column', None)
         quote_policy = deep_merge(
             cls.get_default_quote_policy().to_dict(),
-            source.quoting.to_dict(),
+            source_quoting,
             kwargs.get('quote_policy', {}),
         )
 
diff --git a/core/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py
--- a/core/dbt/contracts/graph/unparsed.py
+++ b/core/dbt/contracts/graph/unparsed.py
@@ -80,6 +80,7 @@ def __post_init__(self):
 
 @dataclass
 class UnparsedColumn(HasTests):
+    quote: Optional[bool] = None
     tags: List[str] = field(default_factory=list)
 
 
@@ -107,7 +108,7 @@ class UnparsedAnalysisUpdate(HasColumnDocs, HasDocs, HasYamlMetadata):
 
 @dataclass
 class UnparsedNodeUpdate(HasColumnTests, HasTests, HasYamlMetadata):
-    pass
+    quote_columns: Optional[bool] = None
 
 
 @dataclass
@@ -218,6 +219,7 @@ class Quoting(JsonSchemaMixin, Mergeable):
     database: Optional[bool] = None
     schema: Optional[bool] = None
     identifier: Optional[bool] = None
+    column: Optional[bool] = None
 
 
 @dataclass
diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py
--- a/core/dbt/parser/schemas.py
+++ b/core/dbt/parser/schemas.py
@@ -8,7 +8,7 @@
 
 from hologram import ValidationError
 
-
+from dbt.adapters.factory import get_adapter
 from dbt.clients.jinja import get_rendered
 from dbt.clients.yaml_helper import load_yaml_text
 from dbt.config import RuntimeConfig, ConfigRenderer
@@ -251,6 +251,12 @@ def parse_test(
             column_tags: List[str] = []
         else:
             column_name = column.name
+            should_quote = (
+                column.quote or
+                (column.quote is None and target_block.quote_columns)
+            )
+            if should_quote:
+                column_name = get_adapter(self.root_project).quote(column_name)
             column_tags = column.tags
 
         block = SchemaTestBlock.from_test_block(


Test patch:
diff --git a/core/dbt/parser/schema_test_builders.py b/core/dbt/parser/schema_test_builders.py
--- a/core/dbt/parser/schema_test_builders.py
+++ b/core/dbt/parser/schema_test_builders.py
@@ -81,6 +81,15 @@ class SourceTarget:
     def name(self) -> str:
         return '{0.name}_{1.name}'.format(self.source, self.table)
 
+    @property
+    def quote_columns(self) -> Optional[bool]:
+        result = None
+        if self.source.quoting.column is not None:
+            result = self.source.quoting.column
+        if self.table.quoting.column is not None:
+            result = self.table.quoting.column
+        return result
+
     @property
     def columns(self) -> Sequence[UnparsedColumn]:
         if self.table.columns is None:
@@ -160,6 +169,10 @@ def tests(self) -> List[TestDef]:
         else:
             return self.target.tests
 
+    @property
+    def quote_columns(self) -> Optional[bool]:
+        return self.target.quote_columns
+
     @classmethod
     def from_yaml_block(
         cls, src: YamlBlock, target: Testable
diff --git a/test/integration/008_schema_tests_test/quote-required-models/model.sql b/test/integration/008_schema_tests_test/quote-required-models/model.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/008_schema_tests_test/quote-required-models/model.sql
@@ -0,0 +1 @@
+select 1 as "Id"
diff --git a/test/integration/008_schema_tests_test/quote-required-models/model_again.sql b/test/integration/008_schema_tests_test/quote-required-models/model_again.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/008_schema_tests_test/quote-required-models/model_again.sql
@@ -0,0 +1 @@
+select 1 as "Id"
diff --git a/test/integration/008_schema_tests_test/quote-required-models/model_noquote.sql b/test/integration/008_schema_tests_test/quote-required-models/model_noquote.sql
new file mode 100644
--- /dev/null
+++ b/test/integration/008_schema_tests_test/quote-required-models/model_noquote.sql
@@ -0,0 +1 @@
+select 1 as id
diff --git a/test/integration/008_schema_tests_test/quote-required-models/schema.yml b/test/integration/008_schema_tests_test/quote-required-models/schema.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/008_schema_tests_test/quote-required-models/schema.yml
@@ -0,0 +1,61 @@
+version: 2
+
+models:
+  - name: model
+    columns:
+      - name: Id
+        quote: true
+        tests:
+          - unique
+          - not_null
+  - name: model_again
+    quote_columns: true
+    columns:
+      - name: Id
+        tests:
+          - unique
+          - not_null
+  - name: model_noquote
+    quote_columns: true
+    columns:
+      - name: Id
+        quote: false
+        tests:
+          - unique
+          - not_null
+
+sources:
+  # this should result in column quoting = true
+  - name: my_source
+    schema: "{{ target.schema }}"
+    quoting:
+      column: true
+    tables:
+      - name: model
+        quoting:
+          column: false
+        columns:
+          - name: Id
+            quote: true
+            tests:
+              - unique
+  - name: my_source_2
+    schema: "{{ target.schema }}"
+    quoting:
+      column: false
+    tables:
+      # this should result in column quoting = true
+      - name: model
+        quoting:
+          column: true
+        columns:
+          - name: Id
+            tests:
+              - unique
+      # this should result in column quoting = false
+      - name: model_noquote
+        columns:
+          - name: Id
+            tests:
+              - unique
+
diff --git a/test/integration/008_schema_tests_test/test_schema_v2_tests.py b/test/integration/008_schema_tests_test/test_schema_v2_tests.py
--- a/test/integration/008_schema_tests_test/test_schema_v2_tests.py
+++ b/test/integration/008_schema_tests_test/test_schema_v2_tests.py
@@ -312,3 +312,28 @@ def test_schema_tests_bigquery(self):
                 )
 
         self.assertEqual(sum(x.status for x in test_results), 0)
+
+
+class TestQuotedSchemaTestColumns(DBTIntegrationTest):
+    @property
+    def schema(self):
+        return "schema_tests_008"
+
+    @property
+    def models(self):
+        return "quote-required-models"
+
+    @use_profile('postgres')
+    def test_postgres_quote_required_column(self):
+        results = self.run_dbt()
+        self.assertEqual(len(results), 3)
+        results = self.run_dbt(['test', '-m', 'model'])
+        self.assertEqual(len(results), 2)
+        results = self.run_dbt(['test', '-m', 'model_again'])
+        self.assertEqual(len(results), 2)
+        results = self.run_dbt(['test', '-m', 'model_noquote'])
+        self.assertEqual(len(results), 2)
+        results = self.run_dbt(['test', '-m', 'source:my_source'])
+        self.assertEqual(len(results), 1)
+        results = self.run_dbt(['test', '-m', 'source:my_source_2'])
+        self.assertEqual(len(results), 2)
diff --git a/test/integration/029_docs_generate_tests/test_docs_generate.py b/test/integration/029_docs_generate_tests/test_docs_generate.py
--- a/test/integration/029_docs_generate_tests/test_docs_generate.py
+++ b/test/integration/029_docs_generate_tests/test_docs_generate.py
@@ -1608,6 +1608,7 @@ def expected_postgres_references_manifest(self, model_database=None):
                         'database': False,
                         'schema': None,
                         'identifier': True,
+                        'column': None,
                     },
                     'database': self.default_database,
                     'description': 'My table',