|
| 1 | +{% macro sqlserver__table_dml_refresh(target_relation, sql) %} |
| 2 | + {# |
| 3 | + DML-only table refresh for use under RCSI. |
| 4 | + |
| 5 | + Instead of rename-swap (which uses DDL and creates a window where the |
| 6 | + table name doesnt resolve), this macro: |
| 7 | + 1. Builds new data into a scratch table via SELECT INTO (minimally logged) |
| 8 | + 2. Compares schemas — if columns changed, falls back to rename-swap |
| 9 | + 3. Swaps data via DELETE + INSERT inside an explicit transaction |
| 10 | + (RCSI ensures concurrent readers see old data until COMMIT) |
| 11 | + 4. Cleans up the scratch table |
| 12 | + |
| 13 | + The scratch table is a regular table with a __dbt_refresh suffix, |
| 14 | + not a global temp table. This avoids cross-session visibility issues |
| 15 | + and ensures cleanup on failure (DROP IF EXISTS at the start of each run). |
| 16 | + #} |
| 17 | + |
| 18 | + {%- set refresh_relation = target_relation.incorporate( |
| 19 | + path={"identifier": target_relation.identifier ~ '__dbt_refresh'} |
| 20 | + ) -%} |
| 21 | + {%- set tmp_vw_relation = refresh_relation.incorporate( |
| 22 | + path={"identifier": refresh_relation.identifier ~ '__dbt_tmp_vw'} |
| 23 | + ) -%} |
| 24 | + |
| 25 | + {# Clean up any leftovers from a prior failed run #} |
| 26 | + {% call statement('dml_refresh_cleanup_pre') -%} |
| 27 | + DROP VIEW IF EXISTS {{ tmp_vw_relation.include(database=False) }}; |
| 28 | + DROP TABLE IF EXISTS {{ refresh_relation }}; |
| 29 | + {%- endcall %} |
| 30 | + |
| 31 | + {# Build new data into scratch table via temp view (handles CTEs in model SQL) #} |
| 32 | + {# Named 'main' because dbt requires a statement('main') call in every materialization #} |
| 33 | + {% call statement('dml_refresh_create_view') -%} |
| 34 | + {{ get_create_view_as_sql(tmp_vw_relation, sql) }} |
| 35 | + {%- endcall %} |
| 36 | + |
| 37 | + {% call statement('main') -%} |
| 38 | + SELECT * INTO {{ refresh_relation }} FROM {{ tmp_vw_relation }}; |
| 39 | + {%- endcall %} |
| 40 | + |
| 41 | + {% call statement('dml_refresh_drop_view') -%} |
| 42 | + DROP VIEW IF EXISTS {{ tmp_vw_relation.include(database=False) }}; |
| 43 | + {%- endcall %} |
| 44 | + |
| 45 | + {# Compare schemas: if columns differ, fall back to rename-swap #} |
| 46 | + {%- set schema_changes = check_for_schema_changes(refresh_relation, target_relation) -%} |
| 47 | + {%- set schema_match = not schema_changes['schema_changed'] -%} |
| 48 | + |
| 49 | + {% if schema_match %} |
| 50 | + {# Use the target's physical column order for both INSERT and SELECT. #} |
| 51 | + {# The scratch table has the same columns but possibly in a different order, #} |
| 52 | + {# so naming columns explicitly makes the swap order-independent. #} |
| 53 | + {%- set target_columns = adapter.get_columns_in_relation(target_relation) -%} |
| 54 | + {%- set column_list = target_columns | map(attribute='quoted') | join(', ') -%} |
| 55 | + |
| 56 | + {# Atomic DML swap — RCSI protects concurrent readers #} |
| 57 | + {# dbt-sqlserver uses autocommit=True and add_begin_query/add_commit_query #} |
| 58 | + {# are no-ops, so this creates a simple (non-nested) transaction. #} |
| 59 | + {% call statement('dml_refresh_swap') -%} |
| 60 | + BEGIN TRANSACTION; |
| 61 | + DELETE FROM {{ target_relation }}; |
| 62 | + INSERT INTO {{ target_relation }} ({{ column_list }}) |
| 63 | + SELECT {{ column_list }} FROM {{ refresh_relation }}; |
| 64 | + COMMIT TRANSACTION; |
| 65 | + {%- endcall %} |
| 66 | + |
| 67 | + {# Cleanup scratch table #} |
| 68 | + {% call statement('dml_refresh_cleanup_post') -%} |
| 69 | + DROP TABLE IF EXISTS {{ refresh_relation }}; |
| 70 | + {%- endcall %} |
| 71 | + |
| 72 | + {% else %} |
| 73 | + {# Schema changed — fall back to rename-swap for this run #} |
| 74 | + {{ log("Schema change detected for " ~ target_relation ~ " — falling back to rename-swap", info=true) }} |
| 75 | + |
| 76 | + {%- set backup_relation_type = target_relation.type -%} |
| 77 | + {%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%} |
| 78 | + {{ drop_relation_if_exists(backup_relation) }} |
| 79 | + |
| 80 | + {# Rename scratch table into position #} |
| 81 | + {% set existing_relation = load_cached_relation(target_relation) %} |
| 82 | + {% if existing_relation is not none %} |
| 83 | + {{ adapter.rename_relation(existing_relation, backup_relation) }} |
| 84 | + {% endif %} |
| 85 | + |
| 86 | + {{ adapter.rename_relation(refresh_relation, target_relation) }} |
| 87 | + |
| 88 | + {% do create_indexes(target_relation) %} |
| 89 | + |
| 90 | + {{ drop_relation_if_exists(backup_relation) }} |
| 91 | + |
| 92 | + {# scratch table is now the target, nothing to drop #} |
| 93 | + {% endif %} |
| 94 | + |
| 95 | +{% endmacro %} |
0 commit comments