Repository: dbt-labs/dbt-core
Pull request: Fix: Source quoting ignores global configuration
Resolves #10892 (regression)

<!---
  Include the number of the issue addressed by this PR above, if applicable.
  PRs for code changes without an associated issue *will not be merged*.
  See CONTRIBUTING.md for more information.

  Add the `user docs` label to this PR if it will need docs changes.  An 
  issue will get opened in docs.getdbt.com upon successful merge of this PR.
-->

### Problem

<!---
  Describe the problem this PR is solving. What is the application state
  before this PR is merged?
-->

In dbt-core 1.7 and earlier, quoting behavior for sources was not controlled by the quoting configuration in dbt_project.yml, but newer versions of dbt also apply those settings to sources.

### Solution

<!---
  Describe the way this PR solves the above problem. Add as much detail as you
  can to help reviewers understand your changes. Include any alternatives and
  tradeoffs you considered.
-->
For sources, pass along an 'empty' quoting config as the base config, ignoring anything that's in self.config.quoting (ie dbt_project.yml). This was effectively the case in versions <1.7, which used [create_from_source](https://github.com/dbt-labs/dbt-core/blob/1.7.latest/core/dbt/adapters/base/relation.py#L207-L211) which did not incorporate the config quoting like [create_from_node](https://github.com/dbt-labs/dbt-core/blob/1.7.latest/core/dbt/adapters/base/relation.py#L241) did.

⚠️ Note: 
We are not putting this behind a behaviour flag because: 
* This behaviour was previously intentional and documented ([here](https://docs.getdbt.com/reference/project-configs/quoting) and [here](https://docs.getdbt.com/reference/resource-properties/quoting)). The quoting: config in the dbt_project.yml file is just for creating relations / resolving a ref, and this is truly addressing a regression
* It is highly unlikely that restoring the previous behaviour will cause any issues in existing projects that may be depending on the quoting config being ignored. [By default and in most adapters other than snowflake, quoting is True by default](https://docs.getdbt.com/reference/project-configs/quoting), so sources getting opted into quoting has not been problematic. It's only on snowflake that the regression has been raised because it is `False` by default, and adding quotes has been unsafe to do by default for sources when they otherwise wouldn't have been.
   * In either case, the current behaviour goes counter to our documentation and would not have been safe to rely on.
* We don't want to phase in the new behaviour / deprecate the old behavior. From @jtcohen6: 
  * people should be specifically configuring sources with nonstandard quoting, so for those people this should be no change (and the previous behavior is more desirable)

### Checklist

- [x] I have read [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md) and understand what's expected of me.
- [x] I have run this code in development, and it appears to resolve the stated issue.
- [x] This PR includes tests, or tests are not required or relevant for this PR.
- [x] This PR has no interface changes (e.g., macros, CLI, logs, JSON artifacts, config files, adapter interface, etc.) or this PR has already received feedback and approval from Product or DX.
- [x] This PR includes [type annotations](https://docs.python.org/3/library/typing.html) for new and modified functions.


Problem statement:
[Regression] Quoting behavior for sources
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

In dbt-core 1.7 and earlier, quoting behavior for sources was not controlled by the quoting configuration in dbt_project.yml, but newer versions of dbt also apply those settings to sources.

In older versions of dbt, this was rendered without quotes around the schema name. The change seems to be unintentional, and we would like to return to the previous behavior.


### Steps To Reproduce


```yaml
# dbt_project.yml
quoting:
  schema: True
```

```yaml
# models/sources.yml
sources:
  - name: <sourcename>
...
```

Create a model that references the source, and note that source(<sourcename>, <tablename>) is rendered in recent versions as:

```
with source as (

    select * from <database>."<schema>".<tablename>

)
```

We can work around with a source which specifically overrides this setting:

```yaml
# models/sources.yml
sources:
  - name: <sourcename>
    quoting:
      schema: False
...
```

### Relevant log output

```shell
.
```


### Environment

```markdown
- OS: Ubuntu/macos
- Python: 3.11
- dbt: 1.8.latest
```


### Which database adapter are you using with dbt?

snowflake

### Additional Context

_No response_

Hints from issue discussion:
Internal thread: https://dbt-labs.slack.com/archives/C05FWBP9X1U/p1729130247375769

Commit message: changelog entry

Patch to review:
diff --git a/.changes/unreleased/Fixes-20241023-152054.yaml b/.changes/unreleased/Fixes-20241023-152054.yaml
new file mode 100644
index 00000000000..976f4cf3add
--- /dev/null
+++ b/.changes/unreleased/Fixes-20241023-152054.yaml
@@ -0,0 +1,6 @@
+kind: Fixes
+body: Restore source quoting behaviour when quoting config provided in dbt_project.yml
+time: 2024-10-23T15:20:54.766893-04:00
+custom:
+  Author: michelleark
+  Issue: "10892"
diff --git a/core/dbt/context/providers.py b/core/dbt/context/providers.py
index dfc8c9bb40b..87a11ad69b3 100644
--- a/core/dbt/context/providers.py
+++ b/core/dbt/context/providers.py
@@ -684,8 +684,15 @@ def resolve(self, source_name: str, table_name: str):
                 target_kind="source",
                 disabled=(isinstance(target_source, Disabled)),
             )
+
+        # Source quoting does _not_ respect global configs in dbt_project.yml, as documented here:
+        # https://docs.getdbt.com/reference/project-configs/quoting
+        # Use an object with an empty quoting field to bypass any settings in self.
+        class SourceQuotingConfig:
+            quoting: Dict[str, Any] = {}
+
         return self.Relation.create_from(
-            self.config,
+            SourceQuotingConfig(),
             target_source,
             limit=self.resolve_limit,
             event_time_filter=self.resolve_event_time_filter(target_source),
diff --git a/tests/functional/relation_quoting/test_relation_quoting.py b/tests/functional/relation_quoting/test_relation_quoting.py
new file mode 100644
index 00000000000..0fd5988c1ac
--- /dev/null
+++ b/tests/functional/relation_quoting/test_relation_quoting.py
@@ -0,0 +1,64 @@
+import pytest
+
+from dbt.tests.util import read_file, run_dbt
+
+_SOURCES_YML = """
+sources:
+  - name: source_name
+    database: source_database
+    schema: source_schema
+    tables:
+      - name: customers
+"""
+
+
+class TestSourceQuotingGlobalConfigs:
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        # Postgres quoting configs are True by default -- turn them all to False to show they are not respected during source rendering
+        return {
+            "quoting": {
+                "database": False,
+                "schema": False,
+                "identifier": False,
+            },
+        }
+
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "sources.yml": _SOURCES_YML,
+            "model.sql": "select * from {{ source('source_name', 'customers') }}",
+        }
+
+    def test_sources_ignore_global_quoting_configs(self, project):
+        run_dbt(["compile"])
+
+        generated_sql = read_file("target", "compiled", "test", "models", "model.sql")
+        assert generated_sql == 'select * from "source_database"."source_schema"."customers"'
+
+
+class TestModelQuoting:
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        # Postgres quoting configs are True by default -- turn them all to False to show they are respected during model rendering
+        return {
+            "quoting": {
+                "database": False,
+                "schema": False,
+                "identifier": False,
+            },
+        }
+
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "model.sql": "select 1 as id",
+            "model_downstream.sql": "select * from {{ ref('model') }}",
+        }
+
+    def test_sources_ignore_global_quoting_configs(self, project):
+        run_dbt(["compile"])
+
+        generated_sql = read_file("target", "compiled", "test", "models", "model_downstream.sql")
+        assert generated_sql == f"select * from dbt.{project.test_schema}.model"


Code review comments:
diff_hunk: @@ -684,8 +684,15 @@ def resolve(self, source_name: str, table_name: str):
                 target_kind="source",
                 disabled=(isinstance(target_source, Disabled)),
             )
+
+        # Source quoting does _not_ respect global configs in dbt_project.yml, as documented here:
+        # https://docs.getdbt.com/reference/project-configs/quoting
+        # Use an object with an empty quoting field to bypass any settings in self.
+        class SourceQuotingConfig:
+            quoting: Dict[str, Any] = {}
+
         return self.Relation.create_from(
-            self.config,
+            SourceQuotingConfig(),
path: core/dbt/context/providers.py
text: @author:
An alternative approach would be to extend `BaseRelation.create_from` to accept an `Optional[HasQuoting]` value instead of just `HasQuoting`. That said, the method in dbt-adapters is:
* abstracted to the point where it does not concern itself with the node type (e.g. model vs seed vs source, etc), and this kind of business logic feels better-suited in dbt-core
* overridable across adapters, so could lead to inconsistent handling + need updates across multiple adapters for consistent behaviour

So I went with this approach. Open to feedback :) I opted to define `SourceQuotingBaseConfig` inline here since it quite specialized and likely won't need to be used elsewhere in the codebase.