From d2d5ba66c9b0d905286c5a4bc8fef2eb527c2daf Mon Sep 17 00:00:00 2001 From: Sam Debruyn Date: Tue, 19 May 2026 10:53:21 +0200 Subject: [PATCH] Use intermediate-and-swap pattern in full-refresh incremental path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full-refresh branch of fabric__incremental dropped the existing target before re-creating it. If the subsequent CREATE TABLE AS SELECT failed for any reason — transient Fabric error, query timeout, broken model SQL, capacity throttling, OOM, network hiccup — the user was left with no target table at all. Build the new table into an intermediate relation first, then rename the existing target to a backup, rename the intermediate to the target, and drop the backup. If the build fails, the existing target stays in place. This is the same pattern dbt-postgres, dbt-snowflake, and dbt-spark use. --- .../models/incremental/incremental.sql | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/dbt/include/fabric/macros/materializations/models/incremental/incremental.sql b/dbt/include/fabric/macros/materializations/models/incremental/incremental.sql index f4c3e9fc..d7bfd098 100644 --- a/dbt/include/fabric/macros/materializations/models/incremental/incremental.sql +++ b/dbt/include/fabric/macros/materializations/models/incremental/incremental.sql @@ -24,19 +24,33 @@ {% if existing_relation is none or full_refresh_mode or existing_relation.is_view %} - {% set tmp_vw_relation = target_relation.incorporate(path={"identifier": target_relation.identifier ~ '__dbt_tmp_vw'}, type='view')-%} - -- Dropping temp view relation if it exists + {%- set intermediate_relation = make_intermediate_relation(target_relation) -%} + {%- set backup_relation = make_backup_relation(target_relation, 'table') -%} + {% set tmp_vw_relation = intermediate_relation.incorporate(path={"identifier": intermediate_relation.identifier ~ '__dbt_tmp_vw'}, type='view')-%} + + -- Dropping any leftover intermediate / backup / temp view relations from a previous failed run + {{ adapter.drop_relation(intermediate_relation) }} + {{ adapter.drop_relation(backup_relation) }} {{ adapter.drop_relation(tmp_vw_relation) }} - -- Dropping target relation if exists - {{ adapter.drop_relation(target_relation) }} + -- Build into an intermediate relation. If this fails, the existing target is untouched. {%- call statement('main') -%} - {{ get_create_table_as_sql(False, target_relation, sql)}} + {{ get_create_table_as_sql(False, intermediate_relation, sql)}} {%- endcall -%} -- Dropping temp view relation {{ adapter.drop_relation(tmp_vw_relation) }} + -- Atomic swap: rename existing target to backup (if any), intermediate to target, then drop backup + {%- set target_existing = load_cached_relation(target_relation) -%} + {% if target_existing is not none %} + {{ adapter.rename_relation(target_existing, backup_relation) }} + {% endif %} + {{ adapter.rename_relation(intermediate_relation, target_relation) }} + {% if target_existing is not none %} + {{ adapter.drop_relation(backup_relation) }} + {% endif %} + -- Add constraints including FK relation. {{ build_model_constraints(target_relation) }}