Skip to content

Commit ae5df81

Browse files
Ensure that target_schema from snapshot config is promoted to node level (#8117) (#8229)
(cherry picked from commit fe9c875) Co-authored-by: Gerda Shank <[email protected]>
1 parent 57660c9 commit ae5df81

File tree

5 files changed

+94
-7
lines changed

5 files changed

+94
-7
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: Copy target_schema from config into snapshot node
3+
time: 2023-07-17T16:06:52.957724-04:00
4+
custom:
5+
Author: gshank
6+
Issue: "6745"

core/dbt/contracts/graph/model_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,8 @@ class SnapshotConfig(EmptySnapshotConfig):
619619
@classmethod
620620
def validate(cls, data):
621621
super().validate(data)
622+
# Note: currently you can't just set these keys in schema.yml because this validation
623+
# will fail when parsing the snapshot node.
622624
if not data.get("strategy") or not data.get("unique_key") or not data.get("target_schema"):
623625
raise ValidationError(
624626
"Snapshots must be configured with a 'strategy', 'unique_key', "
@@ -649,6 +651,7 @@ def validate(cls, data):
649651
if data.get("materialized") and data.get("materialized") != "snapshot":
650652
raise ValidationError("A snapshot must have a materialized value of 'snapshot'")
651653

654+
# Called by "calculate_node_config_dict" in ContextConfigGenerator
652655
def finalize_and_validate(self):
653656
data = self.to_dict(omit_none=True)
654657
self.validate(data)

core/dbt/parser/base.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ def __init__(self, config: RuntimeConfig, manifest: Manifest, component: str) ->
102102
self.package_updaters = package_updaters
103103
self.component = component
104104

105-
def __call__(self, parsed_node: Any, config_dict: Dict[str, Any]) -> None:
106-
override = config_dict.get(self.component)
105+
def __call__(self, parsed_node: Any, override: Optional[str]) -> None:
107106
if parsed_node.package_name in self.package_updaters:
108107
new_value = self.package_updaters[parsed_node.package_name](override, parsed_node)
109108
else:
@@ -280,9 +279,19 @@ def update_parsed_node_config_dict(
280279
def update_parsed_node_relation_names(
281280
self, parsed_node: IntermediateNode, config_dict: Dict[str, Any]
282281
) -> None:
283-
self._update_node_database(parsed_node, config_dict)
284-
self._update_node_schema(parsed_node, config_dict)
285-
self._update_node_alias(parsed_node, config_dict)
282+
283+
# These call the RelationUpdate callable to go through generate_name macros
284+
self._update_node_database(parsed_node, config_dict.get("database"))
285+
self._update_node_schema(parsed_node, config_dict.get("schema"))
286+
self._update_node_alias(parsed_node, config_dict.get("alias"))
287+
288+
# Snapshot nodes use special "target_database" and "target_schema" fields for some reason
289+
if parsed_node.resource_type == NodeType.Snapshot:
290+
if "target_database" in config_dict and config_dict["target_database"]:
291+
parsed_node.database = config_dict["target_database"]
292+
if "target_schema" in config_dict and config_dict["target_schema"]:
293+
parsed_node.schema = config_dict["target_schema"]
294+
286295
self._update_node_relation_name(parsed_node)
287296

288297
def update_parsed_node_config(
@@ -349,7 +358,7 @@ def update_parsed_node_config(
349358
# do this once before we parse the node database/schema/alias, so
350359
# parsed_node.config is what it would be if they did nothing
351360
self.update_parsed_node_config_dict(parsed_node, config_dict)
352-
# This updates the node database/schema/alias
361+
# This updates the node database/schema/alias/relation_name
353362
self.update_parsed_node_relation_names(parsed_node, config_dict)
354363

355364
# tests don't have hooks

tests/functional/simple_snapshot/fixtures.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@
9696
owner: 'a_owner'
9797
"""
9898

99+
models__schema_with_target_schema_yml = """
100+
version: 2
101+
snapshots:
102+
- name: snapshot_actual
103+
tests:
104+
- mutually_exclusive_ranges
105+
config:
106+
meta:
107+
owner: 'a_owner'
108+
target_schema: schema_from_schema_yml
109+
"""
110+
99111
models__ref_snapshot_sql = """
100112
select * from {{ ref('snapshot_actual') }}
101113
"""
@@ -281,6 +293,26 @@
281293
{% endsnapshot %}
282294
"""
283295

296+
snapshots_pg__snapshot_no_target_schema_sql = """
297+
{% snapshot snapshot_actual %}
298+
299+
{{
300+
config(
301+
target_database=var('target_database', database),
302+
unique_key='id || ' ~ "'-'" ~ ' || first_name',
303+
strategy='timestamp',
304+
updated_at='updated_at',
305+
)
306+
}}
307+
308+
{% if var('invalidate_hard_deletes', 'false') | as_bool %}
309+
{{ config(invalidate_hard_deletes=True) }}
310+
{% endif %}
311+
312+
select * from {{target.database}}.{{target.schema}}.seed
313+
314+
{% endsnapshot %}
315+
"""
284316

285317
models_slow__gen_sql = """
286318

tests/functional/simple_snapshot/test_basic_snapshot.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from datetime import datetime
33
import pytz
44
import pytest
5-
from dbt.tests.util import run_dbt, check_relations_equal, relation_from_name
5+
from dbt.tests.util import run_dbt, check_relations_equal, relation_from_name, write_file
66
from tests.functional.simple_snapshot.fixtures import (
77
models__schema_yml,
8+
models__schema_with_target_schema_yml,
89
models__ref_snapshot_sql,
910
seeds__seed_newcol_csv,
1011
seeds__seed_csv,
1112
snapshots_pg__snapshot_sql,
13+
snapshots_pg__snapshot_no_target_schema_sql,
1214
macros__test_no_overlaps_sql,
1315
macros_custom_snapshot__custom_sql,
1416
snapshots_pg_custom_namespaced__snapshot_sql,
@@ -123,6 +125,41 @@ def test_basic_ref(self, project):
123125
ref_setup(project, num_snapshot_models=1)
124126

125127

128+
class TestBasicTargetSchemaConfig(Basic):
129+
@pytest.fixture(scope="class")
130+
def snapshots(self):
131+
return {"snapshot.sql": snapshots_pg__snapshot_no_target_schema_sql}
132+
133+
@pytest.fixture(scope="class")
134+
def project_config_update(self, unique_schema):
135+
return {
136+
"snapshots": {
137+
"test": {
138+
"target_schema": unique_schema + "_alt",
139+
}
140+
}
141+
}
142+
143+
def test_target_schema(self, project):
144+
manifest = run_dbt(["parse"])
145+
assert len(manifest.nodes) == 5
146+
# ensure that the schema in the snapshot node is the same as target_schema
147+
snapshot_id = "snapshot.test.snapshot_actual"
148+
snapshot_node = manifest.nodes[snapshot_id]
149+
assert snapshot_node.schema == f"{project.test_schema}_alt"
150+
assert (
151+
snapshot_node.relation_name
152+
== f'"{project.database}"."{project.test_schema}_alt"."snapshot_actual"'
153+
)
154+
assert snapshot_node.meta == {"owner": "a_owner"}
155+
156+
# write out schema.yml file and check again
157+
write_file(models__schema_with_target_schema_yml, "models", "schema.yml")
158+
manifest = run_dbt(["parse"])
159+
snapshot_node = manifest.nodes[snapshot_id]
160+
assert snapshot_node.schema == "schema_from_schema_yml"
161+
162+
126163
class CustomNamespace:
127164
@pytest.fixture(scope="class")
128165
def snapshots(self):

0 commit comments

Comments
 (0)