From 9c4c9033ecd0654f3be317aa08ba3eaa41aef82c Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:40:57 +0200 Subject: [PATCH 1/9] feat(snowflake): add transient tmp relation type for incremental models - Add `tmp_relation_type: transient` config option, which creates a non-session-scoped staging table visible to Snowflake's lineage tracking while avoiding permanent-table fail-safe storage costs - Add `snowflake__resolve_incremental_tmp_relation` dispatch macro, letting users override the schema/database of the tmp relation to avoid name collisions when concurrent runs share the same schema - Fix Iceberg (catalog-linked) incremental models to use transient instead of permanent tmp tables, reducing fail-safe storage costs - Allow `transient` alongside `table` for delete+insert and microbatch strategies, since both are stable across multi-statement executions Closes #1893 --- ...tures-20260424-transient-tmp-relation.yaml | 6 ++ .../macros/materializations/incremental.sql | 72 ++++++++++++----- .../macros/relations/table/create.sql | 21 +++++ .../incremental/test_incremental_transient.py | 78 +++++++++++++++++++ 4 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml create mode 100644 dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py diff --git a/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml b/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml new file mode 100644 index 0000000000..0e6cb60913 --- /dev/null +++ b/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Add `tmp_relation_type=transient` support for incremental models, enabling Snowflake lineage tracking; add `snowflake__resolve_incremental_tmp_relation` dispatch macro for controlling tmp relation destination; fix Iceberg incremental models to use transient tmp tables instead of permanent tables +time: 2026-04-24T00:00:00.000000-00:00 +custom: + Author: b-per + Issue: "1893" diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index 95be218934..54cd320711 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -12,24 +12,35 @@ Low-level specifics: If an invalid option is specified, then we will raise an - excpetion with corresponding message. + exception with a corresponding message. Languages other than SQL (like Python) will use a temporary table. - With the default strategy of merge, the user may choose between a temporary - table and view (defaulting to view). - - The append strategy can use a view because it will run a single INSERT statement. - - When unique_key is none, the delete+insert and microbatch strategies can use a view beacuse a - single INSERT statement is run with no DELETES as part of the statement. - Otherwise, play it safe by using a temporary table. - - Catalog-linked databases (Iceberg tables) does not support using temporary relations. + With the default strategy of merge, the user may choose between a + temporary table and view (defaulting to view). + + The append strategy can use a view because it will run a single INSERT + statement. + + When unique_key is none, the delete+insert and microbatch strategies + can use a view because a single INSERT statement is run with no DELETES + as part of the statement. Otherwise, play it safe by using a table. + + Catalog-linked databases (Iceberg tables) do not support temporary + relations. A transient table is used instead to avoid the fail-safe + storage costs of permanent tables. + + 'transient' is also available as a user-facing tmp_relation_type for + non-Iceberg models. Unlike session-scoped temporary tables, transient + tables are visible to Snowflake's lineage tracking. Note that transient + tables share the regular schema namespace; use the + snowflake__resolve_incremental_tmp_relation dispatch macro to redirect + tmp relations to a dedicated schema to avoid name collisions when + multiple runs share the same target schema. #} */ - {#-- Always use table for catalog-linked databases (Iceberg) --#} + {#-- Always use transient for catalog-linked databases (Iceberg) --#} {% if snowflake__is_catalog_linked_database(relation=config.model) %} - {{ return("table") }} + {{ return("transient") }} {% endif %} {% if language == "python" and tmp_relation_type is not none %} @@ -39,10 +50,10 @@ ) %} {% endif %} - {% if strategy in ["delete+insert", "microbatch"] and tmp_relation_type is not none and tmp_relation_type != "table" and unique_key is not none %} + {% if strategy in ["delete+insert", "microbatch"] and tmp_relation_type is not none and tmp_relation_type not in ("table", "transient") and unique_key is not none %} {% do exceptions.raise_compiler_error( "In order to maintain consistent results when `unique_key` is not none, - the `" ~ strategy ~ "` strategy only supports `table` for `tmp_relation_type` but " + the `" ~ strategy ~ "` strategy only supports `table` or `transient` for `tmp_relation_type` but " ~ tmp_relation_type ~ " was specified." ) %} @@ -54,6 +65,8 @@ {{ return("table") }} {% elif tmp_relation_type == "view" %} {{ return("view") }} + {% elif tmp_relation_type == "transient" %} + {{ return("transient") }} {% elif strategy in ("default", "merge", "append", "insert_overwrite") %} {{ return("view") }} {% elif strategy in ["delete+insert", "microbatch"] and unique_key is none %} @@ -63,6 +76,22 @@ {% endif %} {% endmacro %} + +{% macro snowflake__resolve_incremental_tmp_relation(tmp_relation) %} + {#-- + Dispatch hook for controlling where the incremental tmp relation is + created. Override in your project to redirect to a dedicated scratch + schema and avoid name collisions when multiple runs share the same + target schema. + + Example override: + {% macro my_project__snowflake__resolve_incremental_tmp_relation(tmp_relation) %} + {{ return(tmp_relation.incorporate(schema='scratch')) }} + {% endmacro %} + --#} + {{ return(tmp_relation) }} +{% endmacro %} + {% materialization incremental, adapter='snowflake', supported_languages=['sql', 'python'] -%} {% set original_query_tag = set_query_tag() %} @@ -96,6 +125,7 @@ {% else %} {% set tmp_relation = make_temp_relation(this).incorporate(type=tmp_relation_type) %} {% endif %} + {% set tmp_relation = snowflake__resolve_incremental_tmp_relation(tmp_relation) %} {% set grant_config = config.get('grants') %} @@ -131,15 +161,15 @@ %} {% else %} - {#-- Create the temp relation, either as a view or as a temp table --#} - {% if is_catalog_linked_db %} - {%- call statement('create_tmp_relation', language=language) -%} - {{ create_table_as(False, tmp_relation, compiled_code, language) }} - {%- endcall -%} - {% elif tmp_relation_type == 'view' %} + {#-- Create the temp relation as a view, temp table, or transient table --#} + {% if tmp_relation_type == 'view' %} {%- call statement('create_tmp_relation') -%} {{ snowflake__create_view_as_with_temp_flag(tmp_relation, compiled_code, True) }} {%- endcall -%} + {% elif tmp_relation_type == 'transient' %} + {%- call statement('create_tmp_relation', language=language) -%} + {{ snowflake__create_table_transient_sql(tmp_relation, compiled_code) }} + {%- endcall -%} {% else %} {%- call statement('create_tmp_relation', language=language) -%} {{ create_table_as(True, tmp_relation, compiled_code, language) }} diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql index 8bc92963da..76dfb34917 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql @@ -35,6 +35,27 @@ {% endmacro %} +{% macro snowflake__create_table_transient_sql(relation, compiled_code) -%} +{#- + Implements CREATE TRANSIENT TABLE ... AS SELECT for use as an incremental + tmp relation. Unlike session-scoped temporary tables, transient tables + persist in the catalog (enabling Snowflake lineage tracking) but have no + fail-safe period, avoiding the storage costs of permanent tables. + https://docs.snowflake.com/en/sql-reference/sql/create-table +-#} + +{%- set sql_header = config.get('sql_header', none) -%} +{{ sql_header if sql_header is not none }} + +create or replace transient table {{ relation }} +as ( + {{ compiled_code }} + ) +; + +{%- endmacro %} + + {% macro snowflake__create_table_temporary_sql(relation, compiled_code) -%} {#- Implements CREATE TEMPORARY TABLE and CREATE TEMPORARY TABLE ... AS SELECT: diff --git a/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py b/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py new file mode 100644 index 0000000000..c935d5d11e --- /dev/null +++ b/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py @@ -0,0 +1,78 @@ +import pytest +from dbt.tests.util import run_dbt, relation_from_name + + +_MODEL_TRANSIENT_TMP = """ +{{ config( + materialized='incremental', + unique_key='id', + tmp_relation_type='transient', +) }} + +select 1 as id, 'alice' as name +{% if is_incremental() %} +union all +select 2 as id, 'bob' as name +{% endif %} +""" + +_MODEL_TRANSIENT_TMP_DELETE_INSERT = """ +{{ config( + materialized='incremental', + incremental_strategy='delete+insert', + unique_key='id', + tmp_relation_type='transient', +) }} + +select 1 as id, 'alice' as name +{% if is_incremental() %} +union all +select 2 as id, 'bob' as name +{% endif %} +""" + + +class TestIncrementalTransientTmpRelation: + """tmp_relation_type='transient' creates a transient (not session-scoped) staging + table, enabling Snowflake lineage tracking while avoiding permanent-table + fail-safe storage costs.""" + + @pytest.fixture(scope="class") + def models(self): + return {"transient_incremental.sql": _MODEL_TRANSIENT_TMP} + + def test_incremental_transient_runs(self, project): + run_dbt(["run"]) + run_dbt(["run"]) + run_dbt(["test"]) + + def test_incremental_transient_initial_row_count(self, project): + run_dbt(["run"]) + result = project.run_sql( + "select count(*) as cnt from {database}.{schema}.transient_incremental", + fetch="one", + ) + assert result[0] == 1 + + def test_incremental_transient_second_run_row_count(self, project): + run_dbt(["run"]) + run_dbt(["run"]) + result = project.run_sql( + "select count(*) as cnt from {database}.{schema}.transient_incremental", + fetch="one", + ) + assert result[0] == 2 + + +class TestIncrementalTransientTmpRelationDeleteInsert: + """transient tmp_relation_type is allowed for delete+insert strategy since + transient tables are stable across multiple statements, unlike views.""" + + @pytest.fixture(scope="class") + def models(self): + return {"transient_delete_insert.sql": _MODEL_TRANSIENT_TMP_DELETE_INSERT} + + def test_incremental_transient_delete_insert_runs(self, project): + run_dbt(["run"]) + run_dbt(["run"]) + run_dbt(["test"]) From 94e0828140c5b6f1dbc76bb5de6f1109ba92d209 Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:51:42 +0200 Subject: [PATCH 2/9] fix: use proper dispatch wrapper for resolve_incremental_tmp_relation Adds the generic wrapper macro `resolve_incremental_tmp_relation` that calls adapter.dispatch(), following the standard dbt pattern (same as `set_query_tag`). The materialization calls the wrapper instead of dispatch directly. --- .../macros/materializations/incremental.sql | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index 54cd320711..76e845e155 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -77,15 +77,20 @@ {% endmacro %} +{% macro resolve_incremental_tmp_relation(tmp_relation) %} + {{ return(adapter.dispatch('resolve_incremental_tmp_relation', 'dbt')(tmp_relation)) }} +{% endmacro %} + + {% macro snowflake__resolve_incremental_tmp_relation(tmp_relation) %} {#-- - Dispatch hook for controlling where the incremental tmp relation is - created. Override in your project to redirect to a dedicated scratch - schema and avoid name collisions when multiple runs share the same + Override this macro in your project to control where the incremental + tmp relation is created. Useful for redirecting to a dedicated scratch + schema to avoid name collisions when multiple runs share the same target schema. - Example override: - {% macro my_project__snowflake__resolve_incremental_tmp_relation(tmp_relation) %} + Example: + {% macro snowflake__resolve_incremental_tmp_relation(tmp_relation) %} {{ return(tmp_relation.incorporate(schema='scratch')) }} {% endmacro %} --#} @@ -125,7 +130,7 @@ {% else %} {% set tmp_relation = make_temp_relation(this).incorporate(type=tmp_relation_type) %} {% endif %} - {% set tmp_relation = snowflake__resolve_incremental_tmp_relation(tmp_relation) %} + {% set tmp_relation = resolve_incremental_tmp_relation(tmp_relation) %} {% set grant_config = config.get('grants') %} From e4486cbb8597dd4a605c68e2719e24d2755cbad0 Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:59:12 +0200 Subject: [PATCH 3/9] fix: check language before Iceberg in dbt_snowflake_get_tmp_relation_type Python models must return "table" before the Iceberg CLD check, otherwise a Python incremental model on a catalog-linked database would receive "transient" and have its compiled Python code passed to snowflake__create_table_transient_sql. --- .../macros/materializations/incremental.sql | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index 76e845e155..7819111539 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -38,11 +38,6 @@ multiple runs share the same target schema. #} */ - {#-- Always use transient for catalog-linked databases (Iceberg) --#} - {% if snowflake__is_catalog_linked_database(relation=config.model) %} - {{ return("transient") }} - {% endif %} - {% if language == "python" and tmp_relation_type is not none %} {% do exceptions.raise_compiler_error( "Python models currently only support 'table' for tmp_relation_type but " @@ -50,6 +45,16 @@ ) %} {% endif %} + {#-- Python always uses a temporary table, regardless of other conditions --#} + {% if language != "sql" %} + {{ return("table") }} + {% endif %} + + {#-- Always use transient for catalog-linked databases (Iceberg) --#} + {% if snowflake__is_catalog_linked_database(relation=config.model) %} + {{ return("transient") }} + {% endif %} + {% if strategy in ["delete+insert", "microbatch"] and tmp_relation_type is not none and tmp_relation_type not in ("table", "transient") and unique_key is not none %} {% do exceptions.raise_compiler_error( "In order to maintain consistent results when `unique_key` is not none, @@ -59,9 +64,7 @@ %} {% endif %} - {% if language != "sql" %} - {{ return("table") }} - {% elif tmp_relation_type == "table" %} + {% if tmp_relation_type == "table" %} {{ return("table") }} {% elif tmp_relation_type == "view" %} {{ return("view") }} From 19d045b73d65e03c28782049ae624f44d26b230b Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:20:12 +0200 Subject: [PATCH 4/9] fix(snowflake): revert CLD tmp relation to table (not transient) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLD schemas only support Iceberg tables — transient tables are not allowed, so creating a transient tmp relation would fail. Revert to the original permanent table behavior for CLD incremental models. --- .../include/snowflake/macros/materializations/incremental.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index 7819111539..91b949f5d7 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -50,9 +50,9 @@ {{ return("table") }} {% endif %} - {#-- Always use transient for catalog-linked databases (Iceberg) --#} + {#-- CLD schemas only support Iceberg tables; use table (not transient) --#} {% if snowflake__is_catalog_linked_database(relation=config.model) %} - {{ return("transient") }} + {{ return("table") }} {% endif %} {% if strategy in ["delete+insert", "microbatch"] and tmp_relation_type is not none and tmp_relation_type not in ("table", "transient") and unique_key is not none %} From 9ac58c1450e0c365d05d4d273fa16ffea13ead1c Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:55:29 +0200 Subject: [PATCH 5/9] fix(snowflake): add Transient to SnowflakeRelationType enum incorporate(type='transient') failed validation because 'transient' was not a recognized value in the enum. --- .../src/dbt/adapters/snowflake/relation_configs/policies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py b/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py index d52733f084..b532250304 100644 --- a/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py +++ b/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py @@ -11,6 +11,7 @@ class SnowflakeRelationType(StrEnum): External = "external" DynamicTable = "dynamic_table" Function = "function" + Transient = "transient" class SnowflakeIncludePolicy(Policy): From 2c20032f13a5d1ce77244f7dc59c8c83081460bf Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:20:36 +0200 Subject: [PATCH 6/9] fix(snowflake): address code review feedback on transient tmp relation - Use type='table' when building the transient tmp relation object so DROP TABLE is emitted correctly (DROP TRANSIENT is invalid SQL) - Add contract enforcement to snowflake__create_table_transient_sql, matching the behavior of the temporary table path - Consolidate three separate test methods into one to reduce CI runtime - Remove unused relation_from_name import from test file --- .../macros/materializations/incremental.sql | 4 +++- .../snowflake/macros/relations/table/create.sql | 9 +++++++++ .../incremental/test_incremental_transient.py | 13 ++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index 91b949f5d7..bde34cd95f 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -131,7 +131,9 @@ {% if is_catalog_linked_db %} {% set tmp_relation = make_temp_relation(this).incorporate(type=tmp_relation_type, catalog=catalog_relation.catalog_name, is_table=true) %} {% else %} - {% set tmp_relation = make_temp_relation(this).incorporate(type=tmp_relation_type) %} + {#-- Transient tables are dropped with DROP TABLE, so the relation type must be 'table' --#} + {% set tmp_relation_object_type = 'table' if tmp_relation_type == 'transient' else tmp_relation_type %} + {% set tmp_relation = make_temp_relation(this).incorporate(type=tmp_relation_object_type) %} {% endif %} {% set tmp_relation = resolve_incremental_tmp_relation(tmp_relation) %} diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql index 76dfb34917..d6fb3c7607 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/relations/table/create.sql @@ -44,10 +44,19 @@ https://docs.snowflake.com/en/sql-reference/sql/create-table -#} +{%- set contract_config = config.get('contract') -%} +{%- if contract_config.enforced -%} + {{- get_assert_columns_equivalent(compiled_code) -}} + {%- set compiled_code = get_select_subquery(compiled_code) -%} +{%- endif -%} + {%- set sql_header = config.get('sql_header', none) -%} {{ sql_header if sql_header is not none }} create or replace transient table {{ relation }} + {%- if contract_config.enforced %} + {{ get_table_columns_and_constraints() }} + {%- endif %} as ( {{ compiled_code }} ) diff --git a/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py b/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py index c935d5d11e..66f52ba208 100644 --- a/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py +++ b/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py @@ -1,5 +1,5 @@ import pytest -from dbt.tests.util import run_dbt, relation_from_name +from dbt.tests.util import run_dbt _MODEL_TRANSIENT_TMP = """ @@ -41,12 +41,7 @@ class TestIncrementalTransientTmpRelation: def models(self): return {"transient_incremental.sql": _MODEL_TRANSIENT_TMP} - def test_incremental_transient_runs(self, project): - run_dbt(["run"]) - run_dbt(["run"]) - run_dbt(["test"]) - - def test_incremental_transient_initial_row_count(self, project): + def test_incremental_transient(self, project): run_dbt(["run"]) result = project.run_sql( "select count(*) as cnt from {database}.{schema}.transient_incremental", @@ -54,8 +49,6 @@ def test_incremental_transient_initial_row_count(self, project): ) assert result[0] == 1 - def test_incremental_transient_second_run_row_count(self, project): - run_dbt(["run"]) run_dbt(["run"]) result = project.run_sql( "select count(*) as cnt from {database}.{schema}.transient_incremental", @@ -63,6 +56,8 @@ def test_incremental_transient_second_run_row_count(self, project): ) assert result[0] == 2 + run_dbt(["test"]) + class TestIncrementalTransientTmpRelationDeleteInsert: """transient tmp_relation_type is allowed for delete+insert strategy since From e74ae64b19c30ded1758ebf10f00362737352fd9 Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:35:22 +0200 Subject: [PATCH 7/9] fix(snowflake): address second round of code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix docstring: CLD schemas only support Iceberg tables, so the tmp relation remains a permanent table (not transient) - Remove unused SnowflakeRelationType.Transient enum value — transient tmp relations use type='table' for the relation object - Remove incorrect Iceberg claim from changelog entry --- .../unreleased/Features-20260424-transient-tmp-relation.yaml | 2 +- .../src/dbt/adapters/snowflake/relation_configs/policies.py | 1 - .../include/snowflake/macros/materializations/incremental.sql | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml b/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml index 0e6cb60913..4cfae04fc3 100644 --- a/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml +++ b/dbt-snowflake/.changes/unreleased/Features-20260424-transient-tmp-relation.yaml @@ -1,5 +1,5 @@ kind: Features -body: Add `tmp_relation_type=transient` support for incremental models, enabling Snowflake lineage tracking; add `snowflake__resolve_incremental_tmp_relation` dispatch macro for controlling tmp relation destination; fix Iceberg incremental models to use transient tmp tables instead of permanent tables +body: Add `tmp_relation_type=transient` support for incremental models, enabling Snowflake lineage tracking; add `snowflake__resolve_incremental_tmp_relation` dispatch macro for controlling tmp relation destination time: 2026-04-24T00:00:00.000000-00:00 custom: Author: b-per diff --git a/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py b/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py index b532250304..d52733f084 100644 --- a/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py +++ b/dbt-snowflake/src/dbt/adapters/snowflake/relation_configs/policies.py @@ -11,7 +11,6 @@ class SnowflakeRelationType(StrEnum): External = "external" DynamicTable = "dynamic_table" Function = "function" - Transient = "transient" class SnowflakeIncludePolicy(Policy): diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index bde34cd95f..6e1a726668 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -26,8 +26,8 @@ as part of the statement. Otherwise, play it safe by using a table. Catalog-linked databases (Iceberg tables) do not support temporary - relations. A transient table is used instead to avoid the fail-safe - storage costs of permanent tables. + relations or transient tables — only Iceberg tables are allowed. A + permanent table is used as the tmp relation for CLD models. 'transient' is also available as a user-facing tmp_relation_type for non-Iceberg models. Unlike session-scoped temporary tables, transient From 12320acd51f944ef1f08ff8154bec4eb9be133a1 Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:45:37 +0200 Subject: [PATCH 8/9] test(snowflake): assert transient DDL is emitted in incremental tests Use run_dbt_and_capture with --debug to verify the tmp relation is actually created as a transient table, not just that the run succeeds. --- .../adapter/incremental/test_incremental_transient.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py b/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py index 66f52ba208..fe06448c97 100644 --- a/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py +++ b/dbt-snowflake/tests/functional/adapter/incremental/test_incremental_transient.py @@ -1,5 +1,5 @@ import pytest -from dbt.tests.util import run_dbt +from dbt.tests.util import run_dbt, run_dbt_and_capture _MODEL_TRANSIENT_TMP = """ @@ -49,7 +49,9 @@ def test_incremental_transient(self, project): ) assert result[0] == 1 - run_dbt(["run"]) + _, logs = run_dbt_and_capture(["--debug", "run"]) + assert "create or replace transient table" in logs.lower() + result = project.run_sql( "select count(*) as cnt from {database}.{schema}.transient_incremental", fetch="one", @@ -69,5 +71,8 @@ def models(self): def test_incremental_transient_delete_insert_runs(self, project): run_dbt(["run"]) - run_dbt(["run"]) + + _, logs = run_dbt_and_capture(["--debug", "run"]) + assert "create or replace transient table" in logs.lower() + run_dbt(["test"]) From 569e0008b1d07573252172f4127a5c16eef2b218 Mon Sep 17 00:00:00 2001 From: Benoit Perigaud <8754100+b-per@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:03:54 +0200 Subject: [PATCH 9/9] fix(snowflake): restore CLD tmp relation creation path for incremental models CLD (catalog-linked database) models require a non-temporary Iceberg table as the tmp relation; use create_table_as(False, ...) for CLD so the DDL follows the Iceberg path rather than the temporary-table path. --- .../snowflake/macros/materializations/incremental.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql index 6e1a726668..7cf18a64b5 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/incremental.sql @@ -172,7 +172,11 @@ {% else %} {#-- Create the temp relation as a view, temp table, or transient table --#} - {% if tmp_relation_type == 'view' %} + {% if is_catalog_linked_db %} + {%- call statement('create_tmp_relation', language=language) -%} + {{ create_table_as(False, tmp_relation, compiled_code, language) }} + {%- endcall -%} + {% elif tmp_relation_type == 'view' %} {%- call statement('create_tmp_relation') -%} {{ snowflake__create_view_as_with_temp_flag(tmp_relation, compiled_code, True) }} {%- endcall -%}