Skip to content

Commit 4e020c6

Browse files
skip clone if target and defer relation are the same (#1198)
1 parent 26d5493 commit 4e020c6

File tree

7 files changed

+131
-11
lines changed

7 files changed

+131
-11
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: Skip cloning objects if the target and defferal are the same
3+
time: 2025-07-14T10:30:18.802612-07:00
4+
custom:
5+
Author: colin-rogers-dbt
6+
Issue: "1198"

dbt-adapters/src/dbt/include/global_project/macros/materializations/models/clone/clone.sql

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@
3232
{% endif %}
3333

3434
-- as a general rule, data platforms that can clone tables can also do atomic 'create or replace'
35-
{% call statement('main') %}
36-
{% if target_relation and defer_relation and target_relation == defer_relation %}
37-
{{ log("Target relation and defer relation are the same, skipping clone for relation: " ~ target_relation.render()) }}
38-
{% else %}
39-
{{ create_or_replace_clone(target_relation, defer_relation) }}
40-
{% endif %}
41-
42-
{% endcall %}
43-
35+
{% if target_relation.database == defer_relation.database and
36+
target_relation.schema == defer_relation.schema and
37+
target_relation.identifier == defer_relation.identifier %}
38+
{{ log("Target relation and defer relation are the same, skipping clone for relation: " ~ target_relation.render()) }}
39+
{% else %}
40+
{% call statement('main') %}
41+
{{ create_or_replace_clone(target_relation, defer_relation) }}
42+
{% endcall %}
43+
{% endif %}
4444
{% set should_revoke = should_revoke(existing_relation, full_refresh_mode=True) %}
4545
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
4646
{% do persist_docs(target_relation, model) %}

dbt-bigquery/tests/functional/adapter/dbt_clone/test_dbt_clone.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import pytest
2-
from dbt.tests.adapter.dbt_clone.test_dbt_clone import BaseClonePossible
2+
from dbt.tests.adapter.dbt_clone.test_dbt_clone import (
3+
BaseClonePossible,
4+
BaseCloneSameSourceAndTarget,
5+
BaseCloneNotPossible,
6+
)
37

48

59
class TestBigQueryClonePossible(BaseClonePossible):
@@ -18,3 +22,11 @@ def clean_up(self, project):
1822
project.adapter.drop_schema(relation)
1923

2024
pass
25+
26+
27+
class TestBigQueryCloneSameSourceAndTarget(BaseCloneSameSourceAndTarget):
28+
pass
29+
30+
31+
class TestBigQueryCloneNotPossible(BaseCloneNotPossible):
32+
pass

dbt-snowflake/tests/functional/adapter/dbt_clone/test_dbt_clone.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import os
44
from copy import deepcopy
55
from dbt.tests.util import run_dbt
6-
from dbt.tests.adapter.dbt_clone.test_dbt_clone import BaseClonePossible
6+
from dbt.tests.adapter.dbt_clone.test_dbt_clone import (
7+
BaseClonePossible,
8+
BaseCloneSameSourceAndTarget,
9+
)
710

811

912
class TestSnowflakeClonePossible(BaseClonePossible):
@@ -80,3 +83,7 @@ def test_can_clone_transient_table(self, project, other_schema):
8083

8184
results = run_dbt(clone_args)
8285
assert len(results) == 1
86+
87+
88+
class TestSnowflakeCloneSameSourceAndTarget(BaseCloneSameSourceAndTarget):
89+
pass
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Add test for scenario where soure/target relations are the same
3+
time: 2025-07-14T10:44:51.045547-07:00
4+
custom:
5+
Author: colin-rogers-dbt
6+
Issue: "1198"

dbt-tests-adapter/src/dbt/tests/adapter/dbt_clone/fixtures.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,50 @@
104104
{{ return(False) }}
105105
{% endmacro %}
106106
"""
107+
source_table_sql = """
108+
{{ config(materialized='table') }}
109+
select 1 as id, 'test_data' as name
110+
union all
111+
select 2 as id, 'more_data' as name
112+
"""
113+
114+
source_based_model_sql = """
115+
{{ config(materialized='table') }}
116+
select * from {{ source('test_source', 'source_table') }}
117+
"""
118+
119+
source_schema_yml = """
120+
version: 2
121+
sources:
122+
- name: test_source
123+
schema: "{{ target.schema }}"
124+
tables:
125+
- name: source_table
126+
models:
127+
- name: source_based_model
128+
columns:
129+
- name: id
130+
data_tests:
131+
- not_null
132+
- unique
133+
- name: name
134+
data_tests:
135+
- not_null
136+
"""
137+
138+
source_based_model_snapshot_sql = """
139+
{% snapshot source_based_model_snapshot %}
140+
141+
{{
142+
config(
143+
target_database=database,
144+
target_schema=schema,
145+
unique_key='id',
146+
strategy='check',
147+
check_cols=['id'],
148+
)
149+
}}
150+
select * from {{ ref('source_based_model') }}
151+
152+
{% endsnapshot %}
153+
"""

dbt-tests-adapter/src/dbt/tests/adapter/dbt_clone/test_dbt_clone.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,48 @@ def test_can_clone_false(self, project, unique_schema, other_schema):
186186
assert all("no-op" in r.message.lower() for r in results)
187187

188188

189+
class BaseCloneSameSourceAndTarget(BaseClone):
190+
def models(self):
191+
return {
192+
"source_based_model.sql": fixtures.source_based_model_sql,
193+
"source_schema.yml": fixtures.source_schema_yml,
194+
}
195+
196+
def snapshots(self):
197+
return {
198+
"snapshot_model.sql": fixtures.source_based_model_snapshot_sql,
199+
}
200+
201+
def test_clone_same_source_and_target(self, project, unique_schema):
202+
"""Test that cloning a table to itself is handled gracefully"""
203+
# Create a source table first
204+
project.run_sql(f"DROP TABLE IF EXISTS {project.database}.{unique_schema}.source_table")
205+
project.run_sql(
206+
f"""
207+
CREATE TABLE {project.database}.{unique_schema}.source_table AS
208+
SELECT 1 as id, 'test_data' as name
209+
UNION ALL
210+
SELECT 2 as id, 'more_data' as name
211+
"""
212+
)
213+
214+
# Run dbt to create the source-based model
215+
run_dbt(["seed"])
216+
run_dbt(["run"])
217+
218+
# Save state
219+
self.copy_state(project.project_root)
220+
221+
# Attempt to clone source_based_model to itself with --full-refresh
222+
# This should not fail but should log a skip message
223+
clone_args = ["clone", "--state", "state", "--full-refresh", "--log-level", "debug"]
224+
225+
_, output = run_dbt_and_capture(clone_args)
226+
227+
# Verify the skip message is logged
228+
assert "skipping clone for relation" in output
229+
230+
189231
class TestPostgresCloneNotPossible(BaseCloneNotPossible):
190232
@pytest.fixture(autouse=True)
191233
def clean_up(self, project):

0 commit comments

Comments
 (0)