<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Dimensional Modelling on Ghost in the data</title><link>https://ghostinthedata.info/tags/dimensional-modelling/</link><description>Ghost in the data</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Ghost in the data</copyright><lastBuildDate>Sun, 14 Jun 2026 09:00:00 +1000</lastBuildDate><atom:link href="https://ghostinthedata.info/tags/dimensional-modelling/index.xml" rel="self" type="application/rss+xml"/><item><title>Ghost Skills: Teaching AI Agents to Think Like Data Engineers</title><link>https://ghostinthedata.info/posts/2026/2026-06-14-ai-ghost-skills-ai-agents/</link><pubDate>Sun, 14 Jun 2026 09:00:00 +1000</pubDate><guid>https://ghostinthedata.info/posts/2026/2026-06-14-ai-ghost-skills-ai-agents/</guid><author>Chris Hillman</author><description>A new open-source skills repo that gives AI coding agents the methodology they keep forgetting — grain, keys, SCD2, incident comms, and more. Tool-agnostic, composable, and opinionated.</description><content:encoded>&lt;p>Another week, another skills repo on the GitHub trending page. I know. There are roughly seventeen of them now, all promising to turn your AI coding agent from a confident intern into a slightly-less-confident intern. Most of them are great. Most of them are also built by solo devs, for solo devs, on solo-dev codebases that fit comfortably in a context window.&lt;/p>
&lt;p>Which is fine, if that&amp;rsquo;s your world.&lt;/p>
&lt;p>Less fine if your world involves a Snowflake warehouse with four tables that &lt;em>could&lt;/em> be the source of truth for &amp;ldquo;customer&amp;rdquo;, an SCD2 someone half-built in 2021 and quietly walked away from, and a dbt project where &lt;code>stg_users_final_v3_actually_use_this&lt;/code> is, somehow, the one you&amp;rsquo;re meant to use. (Don&amp;rsquo;t laugh. You&amp;rsquo;ve seen worse.)&lt;/p>
&lt;p>I&amp;rsquo;ve been using AI agents day-to-day for a while now — mostly as a peer reviewer, a second pair of eyes, a thinking partner that doesn&amp;rsquo;t sigh when I ask it to look at the same query three times. They&amp;rsquo;re genuinely useful.&lt;/p>
&lt;p>So I built something &lt;a href="https://github.com/ghostinthedata-info/skills" target="_blank" rel="noopener">Ghost Skills&lt;/a> — a collection of data-engineering methodology skills for AI coding agents. Before I get into what&amp;rsquo;s in it, let me explain why I bothered.&lt;/p>
&lt;br>
&lt;hr>
&lt;h3 id="the-problem-isnt-the-model-its-that-data-work-has-rules-nobody-told-it-about">The problem isn&amp;rsquo;t the model. It&amp;rsquo;s that data work has rules nobody told it about.&lt;/h3>
&lt;br>
&lt;p>Your AI agent is, by default, an extremely bright graduate on their first day. It knows Python. It knows SQL. It can write a beautifully-commented &lt;code>for&lt;/code> loop and explain window functions to you in five different ways. It will happily generate a fact table, a dbt model, an Airflow DAG, all of it looking very professional.&lt;/p>
&lt;p>What it doesn&amp;rsquo;t know is everything that actually matters.&lt;/p>
&lt;p>It doesn&amp;rsquo;t know that the &lt;code>customer_id&lt;/code> in that source isn&amp;rsquo;t really unique because of the 2022 CRM migration. It doesn&amp;rsquo;t know that Roger pushed a release on Friday afternoon (because of course he did) and the platform&amp;rsquo;s been wheezing through a seven-year backfill ever since. It doesn&amp;rsquo;t know that this dimension changes slowly and the audit team will lose their minds if you flatten the history. It doesn&amp;rsquo;t know that the business definition of &amp;ldquo;active customer&amp;rdquo; changed in March, but only in the marketing data mart, and only sometimes.&lt;/p>
&lt;p>These aren&amp;rsquo;t model intelligence problems. The model is plenty smart. They&amp;rsquo;re &lt;em>context&lt;/em> problems — and the context is exactly the stuff that takes a data engineer two years to learn and roughly thirty minutes to forget when they leave the company.&lt;/p>
&lt;p>What agents are missing isn&amp;rsquo;t capability. It&amp;rsquo;s methodology. It&amp;rsquo;s the durable craft of data engineering — the part that doesn&amp;rsquo;t change when you swap Snowflake for BigQuery, or dbt for SQLMesh, or your warehouse for a &amp;ldquo;lakehouse&amp;rdquo; that&amp;rsquo;s somehow priced like a warehouse anyway.&lt;/p>
&lt;p>That&amp;rsquo;s the gap this is attempting to fill.&lt;/p>
&lt;br>
&lt;hr>
&lt;h3 id="skills-briefly-for-anyone-who-hasnt-drunk-this-particular-kool-aid-yet">Skills, briefly, for anyone who hasn&amp;rsquo;t drunk this particular Kool-Aid yet&lt;/h3>
&lt;br>
&lt;p>If you&amp;rsquo;ve not been living inside Claude Code or Cursor for the last six months: a &lt;em>skill&lt;/em> is a folder of instructions you give an AI coding agent. &amp;ldquo;When you build a fact table, declare the grain first.&amp;rdquo; &amp;ldquo;When you write tests, follow this severity model.&amp;rdquo; &amp;ldquo;When something breaks, here&amp;rsquo;s the comms workflow.&amp;rdquo; You version it, you commit it, you keep it in the repo. The agent reads it before doing the thing, and re-reads it next session because agents have the long-term memory of a goldfish.&lt;/p>
&lt;p>It&amp;rsquo;s a deceptively powerful pattern. Instead of pasting the same prompt every session (and forgetting half of it, and writing it slightly differently this time), the standard lives in the repo. New team member joins? They get the skills. Agent updates next month? It still reads the skills.&lt;/p>
&lt;p>The catch — and we&amp;rsquo;ll come back to this — is that skills tell an agent &lt;em>how&lt;/em> you do something. They don&amp;rsquo;t tell it &lt;em>what&lt;/em> you&amp;rsquo;ve built, &lt;em>why&lt;/em> you built it that way, or &lt;em>which&lt;/em> of your seven utility tables is the one that&amp;rsquo;s still maintained.&lt;/p>
&lt;p>For solo devs, that&amp;rsquo;s fine. There is no &amp;ldquo;seven utility tables&amp;rdquo;. For data teams at any kind of scale, it&amp;rsquo;s a real limit. More on that at the end.&lt;/p>
&lt;br>
&lt;hr>
&lt;h3 id="introducing-ghost-skills">Introducing Ghost Skills&lt;/h3>
&lt;br>
&lt;p>Right. The repo: &lt;a href="https://github.com/ghostinthedata-info/skills" target="_blank" rel="noopener">github.com/ghostinthedata-info/skills&lt;/a>. Built mostly for myself, polished up for sharing.&lt;/p>
&lt;p>30-second setup:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npx skills@latest add ghostinthedata-info/skills
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pick the skills you want, pick your agent (Claude Code, Codex, Cursor — whatever you&amp;rsquo;re using), and run &lt;code>/setup-ghost-skills&lt;/code>. It&amp;rsquo;ll ask you three questions: warehouse dialect, transform tooling, and how your domain docs are laid out. Then it writes itself a configuration block into your &lt;code>CLAUDE.md&lt;/code> or &lt;code>AGENTS.md&lt;/code> and gets out of your way.&lt;/p>
&lt;p>The agent reads it next session. And the one after that. And the one after that. The same standards, every time, without you re-explaining them.&lt;/p>
&lt;p>That&amp;rsquo;s it. That&amp;rsquo;s the whole thing.&lt;/p>
&lt;br>
&lt;hr>
&lt;h3 id="whats-in-the-catalogue">What&amp;rsquo;s in the catalogue&lt;/h3>
&lt;br>
&lt;p>The skills group into four areas, mapped to roughly how data work actually happens.&lt;/p>
&lt;p>&lt;strong>Discovery&lt;/strong> is everything you should do before you build anything, and frequently don&amp;rsquo;t. &lt;code>profile-data&lt;/code> runs the baseline checks on a new dataset — row counts, cardinality, null analysis, key uniqueness — so the agent isn&amp;rsquo;t generating models against assumptions that fall apart in week two. &lt;code>gather-requirements&lt;/code> pins down grain, sources, consumers, freshness SLAs, and acceptance criteria one question at a time, instead of letting the agent guess. &lt;code>refine-context&lt;/code> stress-tests a plan against your documented domain model and writes the decisions back to &lt;code>CONTEXT.md&lt;/code> and ADRs as they crystallise.&lt;/p>
&lt;p>&lt;strong>Modelling&lt;/strong> is the methodology your agent should already be applying and usually isn&amp;rsquo;t. &lt;code>dimensional-modeling&lt;/code> walks the four-step process. &lt;code>fact-table-design&lt;/code> forces grain declaration and measure classification before a single line of SQL gets generated. &lt;code>keys&lt;/code> covers business, natural, surrogate, composite, and durable keys — and the anti-patterns that bite you eighteen months later when the source system gets re-platformed. &lt;code>slowly-changing-dimensions&lt;/code> covers SCD types 0 through 7, including my Healing Tables approach to deterministic, path-independent SCD2.&lt;/p>
&lt;p>&lt;strong>Quality&lt;/strong> is where defensive engineering lives. &lt;code>test-data&lt;/code> produces a test plan you can actually defend in a code review — uniqueness, referential integrity, nulls, accepted values, freshness, volume variance — and maps each test to a severity level. &lt;code>performance-tuning&lt;/code> follows the measure-first philosophy: find the critical path, then fix partition pruning, incremental processing, and the phantom dependencies that are silently doubling your costs. &lt;code>spark-performance&lt;/code> handles the distributed case — partition counts, shuffle minimisation, skew handling, broadcasting small tables.&lt;/p>
&lt;p>&lt;strong>Operations&lt;/strong> is what should happen when things go pop. &lt;code>incident-comms&lt;/code> gives the agent the severity classification, notification workflow, update cadence, and post-incident review template — the &amp;ldquo;Don&amp;rsquo;t Go Dark&amp;rdquo; pattern in skill form. &lt;code>pipeline-design&lt;/code> encodes idempotency, reproducibility, and defensive engineering as defaults instead of afterthoughts. &lt;code>data-as-a-product&lt;/code> brings data mesh thinking into the agent — domain ownership, discoverability, SLAs, federated governance. &lt;code>data-security-classification&lt;/code> covers the four A&amp;rsquo;s, PII/PHI handling, and least privilege.&lt;/p>
&lt;p>There&amp;rsquo;s a setup skill (&lt;code>setup-ghost-skills&lt;/code>) that runs once per repo and handles the configuration. Everything else, you opt into.&lt;/p>
&lt;br>
&lt;hr>
&lt;h3 id="why-tool-agnostic-even-though-tool-specific-would-sell-better">Why tool-agnostic, even though tool-specific would sell better&lt;/h3>
&lt;br>
&lt;p>The temptation when building something like this is to specialise. Snowflake-only. dbt-only. Make every skill assume your exact stack and produce immediately runnable code.&lt;/p>
&lt;p>I deliberately didn&amp;rsquo;t do that, and the tradeoff is worth naming honestly. Generic skills are less immediately useful than highly specific ones. A skill that knows your exact dbt project structure, your column naming conventions, your team&amp;rsquo;s snake_case-vs-camelCase argument that&amp;rsquo;s been going on since 2022 — that&amp;rsquo;s more powerful, day one.&lt;/p>
&lt;p>But it&amp;rsquo;s also yours. It&amp;rsquo;s not shareable. And the bits that are genuinely universal — &lt;em>declare the grain before you build the fact&lt;/em>, &lt;em>a business key isn&amp;rsquo;t a real business key until you&amp;rsquo;ve checked it&amp;rsquo;s unique&lt;/em>, &lt;em>test before publish&lt;/em> — those don&amp;rsquo;t change between platforms. They didn&amp;rsquo;t change when we moved from Teradata to Snowflake. They won&amp;rsquo;t change when we move from Snowflake to whatever everyone&amp;rsquo;s furiously rebranding next year.&lt;/p>
&lt;p>Ghost Skills is the base layer. Your repo&amp;rsquo;s &lt;code>CONTEXT.md&lt;/code> is your specialisation layer. They&amp;rsquo;re meant to work together, not replace each other. Fork it. Add a &lt;code>snowflake-cost-optimisation&lt;/code> skill on top. Open a PR if you want to share it back. (Or don&amp;rsquo;t. Do what you like.)&lt;/p>
&lt;br>
&lt;hr>
&lt;h3 id="skills-help-they-dont-solve-everything-lets-not-pretend">Skills help. They don&amp;rsquo;t solve everything. Let&amp;rsquo;s not pretend.&lt;/h3>
&lt;br>
&lt;p>The honest thing to say at this point is that skills are not a silver bullet, and anyone telling you otherwise is selling you a course.&lt;/p>
&lt;p>Skills tell an agent &lt;em>how&lt;/em> to do things. They don&amp;rsquo;t tell it &lt;em>what&amp;rsquo;s there&lt;/em> or &lt;em>why&lt;/em>. An agent that knows the Kimball four-step process will still make the wrong grain declaration if it doesn&amp;rsquo;t understand the business process it&amp;rsquo;s modelling. A skill can encode what SCD2 is; it can&amp;rsquo;t replace a conversation with the stakeholder who actually needs the history. A skill can encode the Write-Audit-Publish pattern; it can&amp;rsquo;t tell the agent which of your fourteen &amp;ldquo;users&amp;rdquo; tables is the one to run it against.&lt;/p>
&lt;p>The other thing skills can&amp;rsquo;t do is fix a stale skill. The conventions you encode today are the conventions you knew today. If your team&amp;rsquo;s standards evolve — and they should — your skills need to evolve with them. A stale skill is worse than no skill, because it gives the agent a confident wrong answer instead of asking a question.&lt;/p>
&lt;p>So treat them as a starting point. Use them. Fork them. Argue with them in a code review. Update them when your standards shift. The agent will keep reading whatever&amp;rsquo;s in the repo, faithfully, every session — which means the skills are only ever as good as the last person who maintained them.&lt;/p>
&lt;p>The repo is at &lt;a href="https://github.com/ghostinthedata-info/skills" target="_blank" rel="noopener">github.com/ghostinthedata-info/skills&lt;/a>. Fork it for your cloud-specific layer. Open issues if something&amp;rsquo;s wrong. Open a PR if you&amp;rsquo;ve got methodology worth encoding and you don&amp;rsquo;t want to keep it to yourself.&lt;/p>
&lt;p>And if you build something better on top of it, please tell me.&lt;/p>
&lt;p>&lt;br>&lt;br>&lt;/p></content:encoded><category>Data Engineering</category><category>AI</category><category>AI Agents</category><category>Claude Code</category><category>dbt</category><category>Data Modelling</category><category>Dimensional Modelling</category><category>Data Quality</category><category>Open Source</category><category>Snowflake</category></item><item><title>Healing Tables: When Day-by-Day Backfills Become a Slow-Motion Disaster</title><link>https://ghostinthedata.info/posts/2026/2026-02-07-self-healing/</link><pubDate>Sat, 07 Feb 2026 09:00:00 +1100</pubDate><guid>https://ghostinthedata.info/posts/2026/2026-02-07-self-healing/</guid><author>Chris Hillman</author><description>Traditional SCD2 backfilling processes one day at a time, compounding errors and taking forever. The Healing Tables framework offers a better way—rebuild-capable dimensions that can recover from accumulated data quality issues in a single pass.</description><content:encoded>&lt;p>It was 2 AM on a Saturday when I realised we&amp;rsquo;d been loading data wrong for six months.&lt;/p>
&lt;br>
&lt;p>The situation: a customer dimension with three years of history needed to be backfilled after a source system migration. The previous team&amp;rsquo;s approach was straightforward—run the daily incremental process 1,095 times, once for each day of history. They estimated three weeks to complete.&lt;/p>
&lt;p>What they hadn&amp;rsquo;t accounted for was how errors compound. By the time I looked at the data, we had 47,000 records with overlapping date ranges, 12,000 timeline gaps where customers seemed to vanish and reappear, and an unknowable number of missed changes from when source systems updated the same record multiple times in a single day.&lt;/p>
&lt;p>The dimension wasn&amp;rsquo;t just wrong. It was &lt;em>unfixably&lt;/em> wrong using traditional methods. Every incremental run had layered new errors on top of old ones, creating a Jenga tower of data quality issues that couldn&amp;rsquo;t be untangled without starting over.&lt;/p>
&lt;p>That night, I started building what I now call a &lt;strong>Healing Table&lt;/strong>—a dimension that can be completely rebuilt from source data at any point, &amp;ldquo;healing&amp;rdquo; whatever accumulated inconsistencies have crept in over months or years of incremental loads.&lt;/p>
&lt;p>This isn&amp;rsquo;t just a technique for disaster recovery. It&amp;rsquo;s a fundamentally different approach to SCD Type 2 that separates change detection from period construction, processes entire historical ranges in a single pass, and produces dimensions that are deterministically reproducible from source data.&lt;/p>
&lt;p>Here&amp;rsquo;s how it works.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="why-day-by-day-backfills-fail">Why Day-by-Day Backfills Fail&lt;/h2>
&lt;br>
&lt;p>The traditional approach to backfilling historical SCD2 dimensions follows a pattern most data engineers know well: take your daily incremental process, wrap it in a loop, and execute it once for each historical date. Simple, reuses existing code, and catastrophically fragile.&lt;/p>
&lt;p>The problems start immediately.&lt;/p>
&lt;p>&lt;strong>Errors compound across iterations.&lt;/strong> If your change detection logic misses an edge case on January 3rd, that incorrect record becomes the baseline for January 4th&amp;rsquo;s comparison. The error doesn&amp;rsquo;t stay contained—it propagates forward through every subsequent run. By the time you discover the issue months later, you can&amp;rsquo;t simply fix January 3rd because hundreds of downstream records depend on that incorrect state.&lt;/p>
&lt;p>&lt;strong>Performance degrades non-linearly.&lt;/strong> Each daily run needs to compare incoming records against the existing dimension. As the dimension grows through backfill, comparison costs increase. A process that takes 5 minutes for a single day&amp;rsquo;s delta might take 45 minutes per day when the target table contains three years of history. That three-week estimate becomes three months.&lt;/p>
&lt;p>&lt;strong>Source system quirks multiply.&lt;/strong> Real source systems don&amp;rsquo;t change data once per day at a predictable time. They update records multiple times, delete and recreate rows, and occasionally backdate changes. Day-by-day processing either misses these patterns entirely or handles them inconsistently across runs.&lt;/p>
&lt;p>&lt;strong>Recovery requires complete rebuild anyway.&lt;/strong> When—not if—something goes wrong, fixing it requires blowing away the dimension and starting over. But if you&amp;rsquo;re going to rebuild from scratch regardless, why not design for that from the beginning?&lt;/p>
&lt;p>The Healing Tables framework embraces this reality. Instead of trying to prevent rebuilds, it makes them fast, reliable, and deterministic.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="the-six-step-framework">The Six-Step Framework&lt;/h2>
&lt;br>
&lt;p>Healing Tables work by separating two concerns that traditional SCD2 implementations conflate: &lt;strong>change detection&lt;/strong> (identifying when attributes changed) and &lt;strong>period construction&lt;/strong> (building valid time slices with proper start and end dates).&lt;/p>
&lt;p>Traditional approaches detect changes and construct periods simultaneously, comparing incoming records to existing dimension state. This creates tight coupling between current dimension contents and processing logic—if the dimension is wrong, future processing will also be wrong.&lt;/p>
&lt;p>Healing Tables decouple these concerns through a six-step pipeline that operates entirely on source data, constructing the dimension from scratch without reference to any existing target state:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Effectivity Table Creation&lt;/strong> — Extract all change points from sources&lt;/li>
&lt;li>&lt;strong>Time Slice Generation&lt;/strong> — Build date ranges with proper valid_from/valid_to&lt;/li>
&lt;li>&lt;strong>Source Table Joining&lt;/strong> — Conform attributes from multiple sources&lt;/li>
&lt;li>&lt;strong>Hash Computation&lt;/strong> — Enable efficient change detection&lt;/li>
&lt;li>&lt;strong>Row Compression&lt;/strong> — Eliminate consecutive identical states&lt;/li>
&lt;li>&lt;strong>Validation Testing&lt;/strong> — Verify temporal integrity before loading&lt;/li>
&lt;/ol>
&lt;p>Let me walk through each step.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="step-1-building-the-effectivity-table">Step 1: Building the Effectivity Table&lt;/h2>
&lt;br>
&lt;p>The Effectivity Table captures every moment when any tracked attribute changed for any business key. Think of it as a timeline of &amp;ldquo;something happened here&amp;rdquo; markers that we&amp;rsquo;ll later fill in with actual attribute values.&lt;/p>
&lt;p>For sources with explicit timestamps, extraction looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Extract all change points from source
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WITH&lt;/span> source_ordered &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,credit_limit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,updated_at &lt;span style="color:#66d9ef">AS&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,ROW_NUMBER() OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> updated_at
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> version_sequence
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> raw_customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> updated_at &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NOT&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Detect actual changes using LAG comparison
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>change_detection &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(customer_name) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(customer_status) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(credit_limit) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_credit_limit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_ordered
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Keep only first record OR records where attributes actually changed
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,credit_limit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> change_detection
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> version_sequence &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">OR&lt;/span> customer_name &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">DISTINCT&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> prev_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">OR&lt;/span> customer_status &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">DISTINCT&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> prev_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">OR&lt;/span> credit_limit &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">DISTINCT&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> prev_credit_limit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The critical insight here is filtering to &lt;strong>actual changes only&lt;/strong>. Raw source tables often contain records where nothing meaningful changed—perhaps a batch process touched every row, or a status field was updated and then immediately reverted. Including these non-changes creates unnecessary dimension versions that complicate queries and waste storage.&lt;/p>
&lt;p>When working with daily snapshot sources (where you don&amp;rsquo;t have change timestamps, just daily extracts), the pattern shifts to detecting differences between consecutive snapshots:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WITH&lt;/span> daily_ordered &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,snapshot_date
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(customer_name) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> snapshot_date
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(customer_status) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> snapshot_date
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> daily_customer_snapshots
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,snapshot_date &lt;span style="color:#66d9ef">AS&lt;/span> effective_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> daily_ordered
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> prev_name &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span> &lt;span style="color:#75715e">-- First occurrence
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">OR&lt;/span> customer_name &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">DISTINCT&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> prev_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">OR&lt;/span> customer_status &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">DISTINCT&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> prev_status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>IS DISTINCT FROM&lt;/code> operator handles NULL comparisons correctly—a common source of bugs in change detection logic. Standard &lt;code>!=&lt;/code> treats NULL as unknown, meaning &lt;code>NULL != 'value'&lt;/code> returns NULL rather than TRUE. &lt;code>IS DISTINCT FROM&lt;/code> treats NULL as a concrete value, giving us the comparison semantics we actually want.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="step-2-generating-time-slices">Step 2: Generating Time Slices&lt;/h2>
&lt;br>
&lt;p>Time Slices transform point-in-time change events into date ranges with &lt;code>valid_from&lt;/code> and &lt;code>valid_to&lt;/code> columns. The LEAD window function does the heavy lifting, calculating when each version expires based on when the next version begins:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WITH&lt;/span> effectivity_data &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id, customer_name, customer_status, change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> effectivity_table
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,customer_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,change_timestamp &lt;span style="color:#66d9ef">AS&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Calculate valid_to from next version&amp;#39;s start date
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ,COALESCE(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LEAD(change_timestamp) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DATE &lt;span style="color:#e6db74">&amp;#39;9999-12-31&amp;#39;&lt;/span> &lt;span style="color:#75715e">-- High date for current records
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ) &lt;span style="color:#66d9ef">AS&lt;/span> valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Flag current record for easy filtering
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ,&lt;span style="color:#66d9ef">CASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">WHEN&lt;/span> LEAD(change_timestamp) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span> &lt;span style="color:#66d9ef">THEN&lt;/span> &lt;span style="color:#66d9ef">TRUE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ELSE&lt;/span> &lt;span style="color:#66d9ef">FALSE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">END&lt;/span> &lt;span style="color:#66d9ef">AS&lt;/span> is_current
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> effectivity_data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few implementation decisions deserve attention here.&lt;/p>
&lt;p>&lt;strong>High dates versus NULLs for current records.&lt;/strong> Using &lt;code>9999-12-31&lt;/code> for current record end dates simplifies BETWEEN queries but can confuse some BI tools that try to calculate date differences. Using NULL for &lt;code>valid_to&lt;/code> has clearer semantic meaning but requires IS NULL predicates everywhere. My recommendation: use the high date for the column value, but also include an explicit &lt;code>is_current&lt;/code> boolean flag. This gives query authors flexibility without forcing them to remember magic date values.&lt;/p>
&lt;p>&lt;strong>Interval conventions matter.&lt;/strong> Use left-closed, right-open intervals where &lt;code>valid_from &amp;lt;= date &amp;lt; valid_to&lt;/code>. This ensures every point in time maps to exactly one dimension version without ambiguity at boundaries. The alternative—inclusive on both ends—creates situations where midnight timestamps belong to two periods simultaneously.&lt;/p>
&lt;p>&lt;strong>Point-in-time lookup pattern:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> d.&lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">@&lt;/span>lookup_date &lt;span style="color:#f92672">&amp;gt;=&lt;/span> d.valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> &lt;span style="color:#f92672">@&lt;/span>lookup_date &lt;span style="color:#f92672">&amp;lt;&lt;/span> d.valid_to
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="step-3-joining-multiple-sources">Step 3: Joining Multiple Sources&lt;/h2>
&lt;br>
&lt;p>When dimension attributes come from multiple systems, you need to conform those sources before SCD2 processing begins. The temptation is to load directly from multiple sources into a single target—don&amp;rsquo;t do this. You&amp;rsquo;ll create conflicting versions where the same timestamp has different attribute combinations depending on which source processed first.&lt;/p>
&lt;p>The solution is a &lt;strong>Unified Timeline&lt;/strong> that collects all change points from all sources, then joins each source back using as-of-date logic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Step 1: Collect ALL change points from ALL sources
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WITH&lt;/span> unified_change_points &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span> customer_id, change_timestamp &lt;span style="color:#66d9ef">FROM&lt;/span> crm_customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">UNION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span> customer_id, change_timestamp &lt;span style="color:#66d9ef">FROM&lt;/span> erp_customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">UNION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span> customer_id, change_timestamp &lt;span style="color:#66d9ef">FROM&lt;/span> web_profiles
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Step 2: Build time slices from unified timeline
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>unified_time_slices &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,change_timestamp &lt;span style="color:#66d9ef">AS&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,COALESCE(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LEAD(change_timestamp) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> change_timestamp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span> &lt;span style="color:#e6db74">&amp;#39;9999-12-31 23:59:59&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> unified_change_points
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Step 3: Join each source back using as-of logic
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>final_dimension &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ts.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,ts.valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,ts.valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- CRM attributes (authoritative for name)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ,crm.customer_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- ERP attributes (authoritative for financial data)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ,erp.credit_limit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,erp.payment_terms
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Web attributes (authoritative for contact preferences)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ,web.email_opt_in
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,web.preferred_contact_method
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> unified_time_slices ts
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> crm_customers crm
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ON&lt;/span> ts.customer_id &lt;span style="color:#f92672">=&lt;/span> crm.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> crm.change_timestamp &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#66d9ef">MAX&lt;/span>(change_timestamp)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span> crm_customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">WHERE&lt;/span> customer_id &lt;span style="color:#f92672">=&lt;/span> ts.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> change_timestamp &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ts.valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> erp_customers erp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ON&lt;/span> ts.customer_id &lt;span style="color:#f92672">=&lt;/span> erp.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> erp.change_timestamp &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#66d9ef">MAX&lt;/span>(change_timestamp)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span> erp_customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">WHERE&lt;/span> customer_id &lt;span style="color:#f92672">=&lt;/span> ts.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> change_timestamp &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ts.valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> web_profiles web
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ON&lt;/span> ts.customer_id &lt;span style="color:#f92672">=&lt;/span> web.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> web.change_timestamp &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#66d9ef">MAX&lt;/span>(change_timestamp)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span> web_profiles
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">WHERE&lt;/span> customer_id &lt;span style="color:#f92672">=&lt;/span> ts.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> change_timestamp &lt;span style="color:#f92672">&amp;lt;=&lt;/span> ts.valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> final_dimension
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Notice the correlated subqueries for each join—we&amp;rsquo;re finding the most recent version from each source that was effective at each unified timeline point. This handles cases where one source updates frequently while another updates rarely.&lt;/p>
&lt;p>&lt;strong>Document attribute ownership explicitly.&lt;/strong> When CRM and ERP both have a customer name field, which wins? When web profile email conflicts with CRM email, which is authoritative? These decisions belong in documentation and code comments, not in tribal knowledge. Build a table:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Attribute&lt;/th>
 &lt;th>Primary Source&lt;/th>
 &lt;th>Fallback&lt;/th>
 &lt;th>Resolution Rule&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Customer Name&lt;/td>
 &lt;td>CRM&lt;/td>
 &lt;td>ERP&lt;/td>
 &lt;td>CRM always wins&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Credit Limit&lt;/td>
 &lt;td>ERP&lt;/td>
 &lt;td>None&lt;/td>
 &lt;td>ERP authoritative&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Email&lt;/td>
 &lt;td>Web&lt;/td>
 &lt;td>CRM&lt;/td>
 &lt;td>Most recent wins&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Then implement those rules explicitly in your SQL using COALESCE or CASE expressions.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="step-4-hash-computation">Step 4: Hash Computation&lt;/h2>
&lt;br>
&lt;p>Hash-based change detection replaces expensive multi-column comparisons with single-column hash comparisons. The Healing Tables framework uses a &lt;strong>two-hash strategy&lt;/strong>: one hash for business key identification, another for attribute change detection.&lt;/p>
&lt;p>&lt;strong>key_hash&lt;/strong> identifies the business entity. Hash the natural key columns together:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Trino/Starburst syntax
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">lower&lt;/span>(to_hex(sha256(to_utf8(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> COALESCE(&lt;span style="color:#66d9ef">CAST&lt;/span>(source_system &lt;span style="color:#66d9ef">AS&lt;/span> VARCHAR), &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#e6db74">&amp;#39;|&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> COALESCE(&lt;span style="color:#66d9ef">CAST&lt;/span>(customer_id &lt;span style="color:#66d9ef">AS&lt;/span> VARCHAR), &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)))) &lt;span style="color:#66d9ef">AS&lt;/span> key_hash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>row_hash&lt;/strong> detects when any tracked attribute changes. Hash all business attributes excluding keys and metadata:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">lower&lt;/span>(to_hex(sha256(to_utf8(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> COALESCE(customer_name, &lt;span style="color:#e6db74">&amp;#39;^^NULL^^&amp;#39;&lt;/span>) &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#e6db74">&amp;#39;|&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> COALESCE(customer_status, &lt;span style="color:#e6db74">&amp;#39;^^NULL^^&amp;#39;&lt;/span>) &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#e6db74">&amp;#39;|&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> COALESCE(&lt;span style="color:#66d9ef">CAST&lt;/span>(credit_limit &lt;span style="color:#66d9ef">AS&lt;/span> VARCHAR), &lt;span style="color:#e6db74">&amp;#39;^^NULL^^&amp;#39;&lt;/span>) &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#e6db74">&amp;#39;|&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> COALESCE(&lt;span style="color:#66d9ef">CAST&lt;/span>(birth_date &lt;span style="color:#66d9ef">AS&lt;/span> VARCHAR), &lt;span style="color:#e6db74">&amp;#39;^^NULL^^&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)))) &lt;span style="color:#66d9ef">AS&lt;/span> row_hash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>^^NULL^^&lt;/code> placeholder distinguishes NULL values from empty strings—a critical detail that trips up many implementations. Without this, &lt;code>('John', NULL, 'Active')&lt;/code> and &lt;code>('John', '', 'Active')&lt;/code> produce identical hashes despite having semantically different data.&lt;/p>
&lt;p>&lt;strong>Why not use the built-in CONCAT_WS?&lt;/strong> Because CONCAT_WS skips NULL values entirely. The inputs &lt;code>('A', NULL, 'B')&lt;/code> and &lt;code>('A', 'B')&lt;/code> produce identical output, creating false positive matches where none exist. Always wrap columns in COALESCE before concatenation.&lt;/p>
&lt;p>&lt;strong>Column ordering best practice&lt;/strong>: concatenate in alphabetical order. This creates deterministic, reproducible hashes that survive schema evolution—adding a new column doesn&amp;rsquo;t change the hash of existing columns.&lt;/p>
&lt;p>Platform-specific hash functions vary:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Platform&lt;/th>
 &lt;th>Function&lt;/th>
 &lt;th>Syntax&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Snowflake&lt;/td>
 &lt;td>SHA2&lt;/td>
 &lt;td>&lt;code>SHA2(expression, 256)&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>BigQuery&lt;/td>
 &lt;td>SHA256 + TO_HEX&lt;/td>
 &lt;td>&lt;code>TO_HEX(SHA256(expression))&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trino/Starburst&lt;/td>
 &lt;td>sha256 + to_hex&lt;/td>
 &lt;td>&lt;code>to_hex(sha256(to_utf8(expression)))&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SQL Server&lt;/td>
 &lt;td>HASHBYTES&lt;/td>
 &lt;td>&lt;code>HASHBYTES('SHA2_256', expression)&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;br>
&lt;br>
&lt;h2 id="step-5-row-compression">Step 5: Row Compression&lt;/h2>
&lt;br>
&lt;p>Row compression eliminates &lt;strong>consecutive duplicate states&lt;/strong>—adjacent time periods where all attributes are identical. This happens more often than you&amp;rsquo;d expect: source systems frequently touch records without changing meaningful data, or changes get reverted within the same processing window.&lt;/p>
&lt;p>The technique is a classic &lt;strong>Islands and Gaps&lt;/strong> pattern. Assign a group identifier to runs of consecutive identical rows, then collapse each group into a single time slice:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Step 1: Detect group boundaries
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WITH&lt;/span> boundary_detection &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">CASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- New group if: periods don&amp;#39;t connect OR attributes changed
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">WHEN&lt;/span> LAG(valid_to) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#f92672">=&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> row_hash &lt;span style="color:#f92672">=&lt;/span> LAG(row_hash) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">THEN&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#75715e">-- Continuation of previous group
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">ELSE&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e">-- Start of new group
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">END&lt;/span> &lt;span style="color:#66d9ef">AS&lt;/span> is_group_start
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dimension_with_hashes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Step 2: Assign group IDs via running sum
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>group_assignment &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">SUM&lt;/span>(is_group_start) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ROWS&lt;/span> UNBOUNDED PRECEDING
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> group_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> boundary_detection
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Step 3: Collapse each group to single row
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">MIN&lt;/span>(valid_from) &lt;span style="color:#66d9ef">AS&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">MAX&lt;/span>(valid_to) &lt;span style="color:#66d9ef">AS&lt;/span> valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">MAX&lt;/span>(&lt;span style="color:#66d9ef">CASE&lt;/span> &lt;span style="color:#66d9ef">WHEN&lt;/span> is_current &lt;span style="color:#66d9ef">THEN&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">ELSE&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#66d9ef">END&lt;/span>) &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#66d9ef">AS&lt;/span> is_current
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">MAX&lt;/span>(customer_name) &lt;span style="color:#66d9ef">AS&lt;/span> customer_name &lt;span style="color:#75715e">-- All identical within group
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> ,&lt;span style="color:#66d9ef">MAX&lt;/span>(customer_status) &lt;span style="color:#66d9ef">AS&lt;/span> customer_status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">MAX&lt;/span>(row_hash) &lt;span style="color:#66d9ef">AS&lt;/span> row_hash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">MAX&lt;/span>(key_hash) &lt;span style="color:#66d9ef">AS&lt;/span> key_hash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">COUNT&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>) &lt;span style="color:#66d9ef">AS&lt;/span> compressed_row_count &lt;span style="color:#75715e">-- Audit trail
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> group_assignment
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">GROUP&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id, group_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id, valid_from
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two conditions must &lt;strong>both&lt;/strong> be true for rows to compress:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Temporal contiguity&lt;/strong>: Previous row&amp;rsquo;s &lt;code>valid_to&lt;/code> equals current row&amp;rsquo;s &lt;code>valid_from&lt;/code>&lt;/li>
&lt;li>&lt;strong>Attribute identity&lt;/strong>: &lt;code>row_hash&lt;/code> values match&lt;/li>
&lt;/ol>
&lt;p>The contiguity check prevents incorrectly merging records that happen to have identical attributes but represent separate valid periods. If a customer had credit limit 10000 from January to March, then credit limit 5000 from March to June, then credit limit 10000 again from June to December—those are three distinct business states, not one, even though the first and third have identical attribute values.&lt;/p>
&lt;p>Before compression (6 rows):&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>customer_id&lt;/th>
 &lt;th>valid_from&lt;/th>
 &lt;th>valid_to&lt;/th>
 &lt;th>credit_limit&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2020-01-01&lt;/td>
 &lt;td>2020-01-05&lt;/td>
 &lt;td>40000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2020-01-05&lt;/td>
 &lt;td>2020-01-09&lt;/td>
 &lt;td>40000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2020-01-09&lt;/td>
 &lt;td>2020-01-11&lt;/td>
 &lt;td>30000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2022-01-01&lt;/td>
 &lt;td>2022-03-01&lt;/td>
 &lt;td>30000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2022-03-01&lt;/td>
 &lt;td>2022-05-01&lt;/td>
 &lt;td>30000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2022-05-01&lt;/td>
 &lt;td>2022-06-01&lt;/td>
 &lt;td>30000&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>After compression (3 rows):&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>customer_id&lt;/th>
 &lt;th>valid_from&lt;/th>
 &lt;th>valid_to&lt;/th>
 &lt;th>credit_limit&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2020-01-01&lt;/td>
 &lt;td>2020-01-09&lt;/td>
 &lt;td>40000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2020-01-09&lt;/td>
 &lt;td>2020-01-11&lt;/td>
 &lt;td>30000&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1002&lt;/td>
 &lt;td>2022-01-01&lt;/td>
 &lt;td>2022-06-01&lt;/td>
 &lt;td>30000&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>The gap between 2020-01-11 and 2022-01-01 correctly prevents merging the two &amp;ldquo;30000&amp;rdquo; groups—they represent distinct validity periods separated by time when the customer didn&amp;rsquo;t exist in source data.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="step-6-validation-testing">Step 6: Validation Testing&lt;/h2>
&lt;br>
&lt;p>Production SCD2 dimensions require comprehensive validation to ensure temporal integrity. These tests should run after every load and &lt;strong>block pipeline completion on failure&lt;/strong>. Silent failures in dimension loading corrupt downstream fact tables in ways that are extremely difficult to diagnose later.&lt;/p>
&lt;p>&lt;strong>Test 1: Single current record per key&lt;/strong>&lt;/p>
&lt;p>Each business key must have exactly one active (current) record:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- CRITICAL: Fails if any key has multiple current records
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,&lt;span style="color:#66d9ef">COUNT&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>) &lt;span style="color:#66d9ef">AS&lt;/span> current_record_count
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> is_current &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">TRUE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">GROUP&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">HAVING&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">COUNT&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>) &lt;span style="color:#f92672">&amp;lt;&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Expected: Zero rows returned
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Test 2: No overlapping date ranges&lt;/strong>&lt;/p>
&lt;p>Time periods for the same key must not overlap. This is where I learned a painful lesson: the obvious LEAD-based check is &lt;strong>insufficient&lt;/strong>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- INSUFFICIENT - Only checks adjacent records
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WITH&lt;/span> overlap_check &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LEAD(valid_from) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> next_valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> overlap_check
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> next_valid_from &lt;span style="color:#f92672">&amp;lt;=&lt;/span> valid_to
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This misses overlaps between non-adjacent records. Records at positions 1 and 3 could overlap even if record 2 doesn&amp;rsquo;t overlap with either. The comprehensive check requires a self-join:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- COMPREHENSIVE - Checks all pairs
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span> a.&lt;span style="color:#f92672">*&lt;/span>, b.&lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">INNER&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> dim_customer b
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ON&lt;/span> a.customer_id &lt;span style="color:#f92672">=&lt;/span> b.customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> a.dbt_scd_id &lt;span style="color:#f92672">!=&lt;/span> b.dbt_scd_id &lt;span style="color:#75715e">-- Different records
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> a.valid_from &lt;span style="color:#f92672">&amp;lt;&lt;/span> b.valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> a.valid_to &lt;span style="color:#f92672">&amp;gt;&lt;/span> b.valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Expected: Zero rows returned
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Test 3: No timeline gaps&lt;/strong>&lt;/p>
&lt;p>For dimensions requiring continuous history, adjacent periods must connect:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WITH&lt;/span> gap_check &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(valid_to) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,prev_valid_to &lt;span style="color:#66d9ef">AS&lt;/span> gap_start
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,valid_from &lt;span style="color:#66d9ef">AS&lt;/span> gap_end
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> gap_check
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> prev_valid_to &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NOT&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> prev_valid_to &lt;span style="color:#f92672">&amp;lt;&amp;gt;&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Expected: Zero rows (or documented acceptable gaps)
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Test 4: Valid date ordering&lt;/strong>&lt;/p>
&lt;p>Every record must have &lt;code>valid_from &amp;lt; valid_to&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> valid_from &lt;span style="color:#f92672">&amp;gt;=&lt;/span> valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Expected: Zero rows
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Test 5: No consecutive duplicate versions&lt;/strong>&lt;/p>
&lt;p>After compression, no adjacent rows should have identical attributes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WITH&lt;/span> version_check &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,row_hash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(row_hash) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_row_hash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ,LAG(valid_to) OVER (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PARTITION &lt;span style="color:#66d9ef">BY&lt;/span> customer_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> valid_from
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#66d9ef">AS&lt;/span> prev_valid_to
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_customer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> version_check
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> row_hash &lt;span style="color:#f92672">=&lt;/span> prev_row_hash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> valid_from &lt;span style="color:#f92672">=&lt;/span> prev_valid_to &lt;span style="color:#75715e">-- Contiguous AND identical
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Expected: Zero rows
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In dbt, implement these as reusable tests:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># schema.yml&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">models&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">dim_customer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">tests&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">dbt_utils.unique_combination_of_columns&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">combination_of_columns&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">customer_id&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">valid_from&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">columns&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">customer_id&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">tests&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">unique&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">config&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">where&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;is_current = TRUE&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">valid_from&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">tests&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">not_null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">valid_to&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">tests&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">not_null&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="the-self-healing-property">The Self-Healing Property&lt;/h2>
&lt;br>
&lt;p>The name &amp;ldquo;Healing Tables&amp;rdquo; comes from a property that emerges from this design: dimensions built this way can recover from accumulated data quality issues simply by reprocessing source data.&lt;/p>
&lt;p>Traditional incremental SCD2 implementations are &lt;strong>path-dependent&lt;/strong>—the current state depends on the exact sequence of historical loads. If Tuesday&amp;rsquo;s load had a bug, and Wednesday through Friday ran correctly, you can&amp;rsquo;t just re-run Tuesday. Those later loads built on Tuesday&amp;rsquo;s incorrect state.&lt;/p>
&lt;p>Healing Tables are &lt;strong>path-independent&lt;/strong>. The dimension produced depends only on source data contents, not on the history of load operations. Re-run any date range with corrected logic, and the result is identical to what you&amp;rsquo;d get from a full rebuild.&lt;/p>
&lt;p>This property transforms how teams think about data quality issues:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Bug in change detection logic?&lt;/strong> Fix it, reprocess affected date range, done.&lt;/li>
&lt;li>&lt;strong>Source system correction applied retroactively?&lt;/strong> Reprocess includes those corrections automatically.&lt;/li>
&lt;li>&lt;strong>New attribute added to dimension?&lt;/strong> Reprocess rebuilds all historical versions with the new column.&lt;/li>
&lt;li>&lt;strong>Suspected data corruption?&lt;/strong> Rebuild and compare—if results differ, you&amp;rsquo;ve found your problem.&lt;/li>
&lt;/ul>
&lt;p>The dimension becomes &lt;strong>reproducible&lt;/strong> in the same way good software builds are reproducible. Given the same inputs and logic, you always get the same outputs. This isn&amp;rsquo;t just convenient for debugging—it&amp;rsquo;s a fundamental quality characteristic that enables reliable data pipelines.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="when-to-use-this-approach">When to Use This Approach&lt;/h2>
&lt;br>
&lt;p>Healing Tables aren&amp;rsquo;t appropriate for every situation. They work best when:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>You have access to complete source history.&lt;/strong> The pattern assumes you can extract all historical changes, not just deltas. If your source system only provides &amp;ldquo;current state&amp;rdquo; with no history, you&amp;rsquo;ll need a different approach.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Source data is contiguous.&lt;/strong> The timeline construction assumes no gaps in source timestamps. If customers can appear, disappear, and reappear in source data, you&amp;rsquo;ll need additional logic to handle these discontinuities.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Rebuild time is acceptable.&lt;/strong> Processing years of history takes longer than processing a single day&amp;rsquo;s changes. For dimensions with billions of historical rows, full rebuilds might be impractical even with optimisation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Your sources don&amp;rsquo;t have hard deletes.&lt;/strong> The pattern assumes deletions are logical (flagged) rather than physical. If source systems physically remove rows, you&amp;rsquo;ll lose that history.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>Traditional incremental approaches remain appropriate for:&lt;/p>
&lt;ul>
&lt;li>Very high-volume dimensions where full rebuilds are impractical&lt;/li>
&lt;li>Real-time or near-real-time requirements&lt;/li>
&lt;li>Situations where source history is unavailable&lt;/li>
&lt;li>Simple dimensions with stable, well-understood source systems&lt;/li>
&lt;/ul>
&lt;p>The choice isn&amp;rsquo;t binary. Many teams use Healing Tables for periodic &amp;ldquo;refresh&amp;rdquo; operations while running standard incremental processes daily. If the daily process accumulates issues, the weekly or monthly heal repairs them.&lt;/p>
&lt;br>
&lt;br>
&lt;hr>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;br>
&lt;p>That 2 AM debugging session taught me something important: the traditional approach to SCD2—incremental changes applied to existing dimension state—creates tight coupling between historical load operations and current data quality. Every bug, every edge case, every source system quirk gets baked into the dimension permanently.&lt;/p>
&lt;p>Healing Tables break that coupling. By extracting change points from source data, constructing time slices independently, compressing duplicate states, and validating results before loading, you create dimensions that are deterministically reproducible from source data.&lt;/p>
&lt;p>The six-step framework isn&amp;rsquo;t complicated, but it requires discipline:&lt;/p>
&lt;ol>
&lt;li>Build the Effectivity Table with actual changes only&lt;/li>
&lt;li>Generate Time Slices with proper valid_from/valid_to boundaries&lt;/li>
&lt;li>Join multiple sources using unified timeline logic&lt;/li>
&lt;li>Compute hashes for efficient change detection&lt;/li>
&lt;li>Compress consecutive identical states&lt;/li>
&lt;li>Validate temporal integrity before loading&lt;/li>
&lt;/ol>
&lt;p>Implement those steps correctly, and you&amp;rsquo;ll have dimensions that can heal themselves from whatever accumulated issues have crept in over months or years of operation.&lt;/p>
&lt;p>The next time someone asks you to backfill three years of history by running the daily process 1,095 times, you&amp;rsquo;ll have a better answer.&lt;/p>
&lt;p>&lt;br>&lt;br>&lt;/p>
&lt;hr>
&lt;p>&lt;strong>Want the full implementation?&lt;/strong> The &lt;a href="https://ghostinthedata.info/resources/#healing-tables">Healing Tables SCD2 Kit&lt;/a> packages this six-step framework as production SQL for Snowflake, BigQuery, Trino and SQL Server, plus dbt macros and the temporal-integrity tests above, ready to drop into your project. If you&amp;rsquo;d rather have an agent apply it, the &lt;a href="https://github.com/ghostinthedata-info/skills" target="_blank" rel="noopener">Ghost Skills&lt;/a> library includes a &lt;code>data-modelling&lt;/code> skill that follows the same method.&lt;/p></content:encoded><category>Data Engineering</category><category>Data Modelling</category><category>SCD</category><category>Historical Load</category><category>dbt</category><category>SQL</category><category>Data Quality</category><category>Dimensional Modelling</category><category>Delta Lake</category><category>Best Practices</category></item><item><title>That Tuesday Morning When I Finally Fixed Our Ten-Minute Queries</title><link>https://ghostinthedata.info/posts/2026/2026-01-16-aws-dimensional-modelling/</link><pubDate>Fri, 16 Jan 2026 09:00:00 +1100</pubDate><guid>https://ghostinthedata.info/posts/2026/2026-01-16-aws-dimensional-modelling/</guid><author>Chris Hillman</author><description>How Kimball dimensional modelling, proper job orchestration, and Write-Audit-Publish patterns transformed our data pipeline from a nightmare into something that actually works</description><content:encoded>&lt;h3 id="the-ten-minute-query">The Ten-Minute Query&lt;/h3>
&lt;hr>
&lt;p>I&amp;rsquo;m sitting at my laptop on a Tuesday morning, waiting. The progress bar on my screen says &amp;lsquo;Query running&amp;hellip; 4 minutes, 37 seconds.&amp;rsquo; I lean back in my chair and let out this long sigh that probably says more than I intended.&lt;/p>
&lt;p>My manager walks past my desk. She glances at my screen, and I can see that look—the one that says she already knows what I&amp;rsquo;m about to tell her. I didn&amp;rsquo;t need to explain.&lt;/p>
&lt;p>&amp;ldquo;Still pulling that customer report?&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;Yeah.&amp;rdquo;&lt;/p>
&lt;p>She shakes her head. &amp;ldquo;We need to fix this.&amp;rdquo;&lt;/p>
&lt;p>It was an ad-hoc piece of work that we&amp;rsquo;d been running for months. But I just nod.&lt;/p>
&lt;p>Here&amp;rsquo;s what was happening: every time someone needed to know &amp;lsquo;how many orders did customer X place?&amp;rsquo; or &amp;lsquo;which products sold best last month?&amp;rsquo;, we&amp;rsquo;d query this massive table. Three million rows. Every single customer&amp;rsquo;s name, address, email—repeated on every order row. The system was scanning through all that redundant data every single time.&lt;/p>
&lt;p>And then, two weeks ago, it got worse. Someone ran a data import script that had a bug. It replaced half our customer names with NULL. By the time we caught it, twelve reports had already gone out with blank customer names. I had to send apology emails. My team and I had to stay until midnight fixing it.&lt;/p>
&lt;p>I thought, &lt;em>there has to be a better way to do this&lt;/em>. People have been building data systems for decades—someone must have figured this out.&lt;/p>
&lt;p>And they have. His name is Ralph Kimball, and his dimensional modelling methodology has been the gold standard for data warehouses since the 1990s.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h3 id="the-problem-i-couldnt-unsee">The Problem I Couldn&amp;rsquo;t Unsee&lt;/h3>
&lt;hr>
&lt;p>Let me show you what our data looked like.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>order_id | date | customer_name | address | email | product | amount
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------|------------|---------------|--------------|--------------|---------|-------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 | 2024-11-15 | John Smith | 123 Main St | john@... | Pizza | 25
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 | 2024-11-15 | John Smith | 123 Main St | john@... | Soda | 3
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 | 2024-11-15 | Jane Doe | 456 Oak Ave | jane@... | Pizza | 25
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>4 | 2024-11-16 | John Smith | 123 Main St | john@... | Burger | 12
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>See what&amp;rsquo;s happening? John Smith&amp;rsquo;s address and email are stored three times. Multiply that by thousands of customers and millions of orders, and you&amp;rsquo;ve got:&lt;/p>
&lt;ul>
&lt;li>Every query scanning way more data than necessary&lt;/li>
&lt;li>Storage costs that make your CFO unhappy&lt;/li>
&lt;li>One bug corrupting your entire history&lt;/li>
&lt;li>Query times that make you question your career choices&lt;/li>
&lt;/ul>
&lt;p>Every single query was like asking someone to find your keys by searching every room in your house, including the ones you never use. Sure, they&amp;rsquo;ll find the keys eventually. But there&amp;rsquo;s a better way.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h3 id="the-kitchen-that-changed-everything">The Kitchen That Changed Everything&lt;/h3>
&lt;hr>
&lt;p>Let me paint you a hypothetical scenario. Imagine it&amp;rsquo;s a Saturday, and you&amp;rsquo;re standing in your kitchen after just moving house. You can&amp;rsquo;t find anything. Everything&amp;rsquo;s still in boxes.&lt;/p>
&lt;p>You&amp;rsquo;re rummaging through the third box, looking for a spatula. You&amp;rsquo;re pulling out random stuff—a whisk, some Tupperware lids, a pizza cutter, yesterday&amp;rsquo;s takeout containers, a bag of flour. Everything mixed together. This is chaos.&lt;/p>
&lt;p>Now imagine a friend comes over. They take one look at your kitchen and say, &amp;ldquo;What is this?&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;It&amp;rsquo;s&amp;hellip; I haven&amp;rsquo;t unpacked yet.&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;You&amp;rsquo;ve been here a week.&amp;rdquo; They walk over to the mess. &amp;ldquo;Here&amp;rsquo;s what you should do. The stuff that rarely changes—your plates, your pots, your cutting boards—those go in the cabinets. Organised, labelled, easy to find. The stuff that comes and goes constantly—the groceries, the leftovers, what you&amp;rsquo;re cooking today—that goes in the fridge and on the counter. Don&amp;rsquo;t mix the permanent stuff with the temporary stuff.&amp;rdquo;&lt;/p>
&lt;p>Two hours later, the kitchen makes sense. You can find anything in seconds. And here&amp;rsquo;s the wild part—it actually takes up less space because you&amp;rsquo;re not duplicating storage for the same items.&lt;/p>
&lt;p>That&amp;rsquo;s dimensional modelling.&lt;/p>
&lt;p>When your data is like those boxes, everything&amp;rsquo;s mixed together. Customer information sitting right next to order information, repeated over and over. Every time you need something, you&amp;rsquo;re digging through all the boxes.&lt;/p>
&lt;p>Dimensional modelling says: let&amp;rsquo;s organise this. Put stable descriptive data—things that rarely change—in one place. We call these &lt;strong>dimension tables&lt;/strong>. Put the events that happen constantly—the transactions, the updates, the measurements—in another place. We call these &lt;strong>fact tables&lt;/strong>. Link them with keys.&lt;/p>
&lt;p>Here&amp;rsquo;s what that transformation looks like:&lt;/p>
&lt;p>BEFORE (Messy Boxes - everything mixed together):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>order_id | customer_name | address | product | amount
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 | John Smith | 123 Main St | Pizza | 25
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 | John Smith | 123 Main St | Soda | 3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>↑ John&amp;rsquo;s details repeated every single order!&lt;/p>
&lt;p>AFTER (Organised Kitchen):
DIM_CUSTOMER (Cabinets - stable stuff, stored ONCE):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>customer_key | customer_id | name | address | email
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1001 | C-123 | John Smith | 123 Main St | john@...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1002 | C-456 | Jane Doe | 456 Oak Ave | jane@...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DIM_DATE (Calendar on the wall - one row per day):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>date_key | full_date | day_name | month | quarter | year | is_weekend
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>20241115 | 2024-11-15 | Friday | Nov | Q4 | 2024 | N
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>20241116 | 2024-11-16 | Saturday | Nov | Q4 | 2024 | Y
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>FACT_ORDERS (Fridge/counter - events that happen constantly):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>order_key | date_key | customer_key | product_key | amount
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 | 20241115 | 1001 | 5 | 25
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 | 20241115 | 1001 | 8 | 3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Customer details stored one time. Not a thousand times. When you need to answer &amp;lsquo;how much did we sell?&amp;rsquo;, you scan the compact fact table. When you need &amp;lsquo;which customers bought?&amp;rsquo;, you JOIN to get the descriptive details.&lt;/p>
&lt;p>Notice something important in that structure? The dimension tables have two types of keys:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>customer_key&lt;/strong> (1001, 1002) — This is a &lt;em>surrogate key&lt;/em>. A meaningless integer we generate ourselves.&lt;/li>
&lt;li>&lt;strong>customer_id&lt;/strong> (C-123, C-456) — This is the &lt;em>natural key&lt;/em>. The ID from the source system.&lt;/li>
&lt;/ul>
&lt;p>Why both? Because source systems are chaotic. They recycle IDs. They change formats. They get merged in acquisitions. The surrogate key is &lt;em>our&lt;/em> key—stable, predictable, and under our control. Every dimension table gets a surrogate key as its primary key.&lt;/p>
&lt;p>And that date dimension? It&amp;rsquo;s &amp;ldquo;the most important dimension.&amp;rdquo; Every fact table should have one. It lets you slice data by day of week, month, quarter, fiscal period, holidays—without calculating these on every query.&lt;/p>
&lt;p>The core principle: things that rarely change go in dedicated storage, organised and easy to find. Things that happen constantly go in their own space, linked back by keys. Same rule applies to data:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Dimensions&lt;/strong> = Stable descriptive stuff (customers, products, locations, dates)&lt;/li>
&lt;li>&lt;strong>Facts&lt;/strong> = Events that happen constantly (orders, clicks, updates, measurements)&lt;/li>
&lt;/ul>
&lt;/br>
&lt;/br>
&lt;h3 id="the-conference-room-ill-never-forget">The Conference Room I&amp;rsquo;ll Never Forget&lt;/h3>
&lt;hr>
&lt;p>I&amp;rsquo;m sitting in a conference room. Fluorescent lights buzzing. That weird hum they make when it&amp;rsquo;s too quiet—I&amp;rsquo;m wondering why we haven&amp;rsquo;t changed to LEDs. My manager is standing at the whiteboard, and she&amp;rsquo;s pissed.&lt;/p>
&lt;p>&amp;ldquo;Someone,&amp;rdquo; she says, not looking at anyone in particular, &amp;ldquo;pushed bad data to production last night.&amp;rdquo;&lt;/p>
&lt;p>I feel my stomach drop. The data pipeline my team had been working on.&lt;/p>
&lt;p>&amp;ldquo;The revenue report that went to the board this morning showed every transaction as zero dollars. Zero. The board emailed me asking if the company made any money last quarter.&amp;rdquo;&lt;/p>
&lt;p>Oh god. Oh no. I sank down in my chair a little.&lt;/p>
&lt;p>Turns out it wasn&amp;rsquo;t actually our release—it was someone else&amp;rsquo;s. But in that moment, sitting there, I realised something crucial: we had no safety net. Data went straight from transformation to production. No checks. No validation. If you made a mistake, everyone saw it immediately.&lt;/p>
&lt;p>That conference room moment stuck with me. I started researching how other teams prevented this kind of disaster. That&amp;rsquo;s when I learned about Write-Audit-Publish. And honestly? It&amp;rsquo;s probably saved me from that same sick feeling countless times since.&lt;/p>
&lt;p>Here&amp;rsquo;s the idea:&lt;/p>
&lt;p>&lt;strong>WRITE&lt;/strong> — You finish your transformation. Instead of putting the data straight into production, you write it to a staging branch. Think of it like a draft folder for emails. It&amp;rsquo;s there, but it&amp;rsquo;s not official yet.&lt;/p>
&lt;p>&lt;strong>AUDIT&lt;/strong> — This is the crucial part. You run every check you can think of:&lt;/p>
&lt;ul>
&lt;li>Did I lose any rows?&lt;/li>
&lt;li>Are there any NULL values where there shouldn&amp;rsquo;t be?&lt;/li>
&lt;li>Do all the foreign keys match up?&lt;/li>
&lt;li>Does the math make sense?&lt;/li>
&lt;li>Do surrogate keys have duplicates?&lt;/li>
&lt;/ul>
&lt;p>It&amp;rsquo;s like re-reading an important email before you hit send. You check for typos, you verify the recipients, you make sure the attachment is right.&lt;/p>
&lt;p>&lt;strong>Important:&lt;/strong> In the future when you find incidents, bugs, or problems—add them to this AUDIT layer. Ensure you don&amp;rsquo;t make the same mistakes twice.&lt;/p>
&lt;p>&lt;strong>PUBLISH&lt;/strong> — Only if everything passes, you merge the branch to production. If anything fails, you stop. You fix it. You try again. The bad data never reaches production. Never reaches that board meeting.&lt;/p>
&lt;p>The traditional way to implement this involves copying data between folders or tables. But there&amp;rsquo;s a better approach using Apache Iceberg&amp;rsquo;s branching feature. Instead of physically copying data, you create a branch—like in Git. Your audit runs against the branch. If it passes, you merge the branch to main. If it fails, you drop the branch. No data was ever at risk.&lt;/p>
&lt;p>&lt;strong>Critical design decision:&lt;/strong> WAP should happen at &lt;em>each table level&lt;/em>, not just at the end of the pipeline. Each dimension job does its own Write-Audit-Publish cycle. Each fact job does its own. This way:&lt;/p>
&lt;ul>
&lt;li>Failures are isolated to the specific table that failed&lt;/li>
&lt;li>You catch issues early, not at the very end&lt;/li>
&lt;li>Each job is self-contained and independently testable&lt;/li>
&lt;li>Downstream jobs read from published (main) tables, not staging branches&lt;/li>
&lt;/ul>
&lt;p>Let me show you what this looks like when an audit catches something:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>AUDIT PHASE (dim_system):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Row count check.......................... PASS
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Source: 1,198 rows
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Target: 1,198 rows
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Primary key uniqueness................... FAIL
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Found 13 duplicate system_keys in dim_system!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AUDIT FAILED — BRANCH NOT MERGED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Data remains in staging branch.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Production table unchanged.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Fix the duplicates and re-run the job.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Look at that. The audit caught duplicate surrogate keys before they reached production. Nobody sees this except you. You fix the problem. You run it again. Then it passes, and the branch merges to main.&lt;/p>
&lt;p>This is Write-Audit-Publish. It&amp;rsquo;s the difference between sending an email with a typo versus catching it first. Between serving undercooked food versus checking with a thermometer. Between publishing bad data versus validating before it matters.&lt;/p>
&lt;p>That conference room moment? Doesn&amp;rsquo;t happen with this pattern.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h3 id="building-this-thing-for-real">Building This Thing for Real&lt;/h3>
&lt;hr>
&lt;p>Alright, enough theory. Let me show you how to actually build this in AWS.&lt;/p>
&lt;p>We&amp;rsquo;re going to use real (game) space exploration data—systems, planets, updates. It&amp;rsquo;s way more interesting than order data, and the patterns work exactly the same.&lt;/p>
&lt;p>&lt;strong>In this section:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="#architecture-overview">Architecture Overview&lt;/a>&lt;/li>
&lt;li>&lt;a href="#step-1-setting-up-s3-buckets">Step 1: Setting Up S3 Buckets&lt;/a>&lt;/li>
&lt;li>&lt;a href="#step-2-creating-the-iceberg-tables">Step 2: Creating the Iceberg Tables&lt;/a>&lt;/li>
&lt;li>&lt;a href="#the-heart-of-the-system-orchestrated-glue-jobs-with-per-table-wap">The Glue Jobs (with Per-Table WAP)&lt;/a>&lt;/li>
&lt;li>&lt;a href="#step-functions-workflow">Step Functions Workflow&lt;/a>&lt;/li>
&lt;li>&lt;a href="#dependency-triggers-with-s3-events">Dependency Triggers with S3 Events&lt;/a>&lt;/li>
&lt;/ul>
&lt;h4 id="architecture-overview">Architecture Overview&lt;/h4>
&lt;p>Here&amp;rsquo;s our architecture:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> +------------------------------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | AWS Step Functions Workflow |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +------------------------------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +----------------------------+----------------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v v v
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +------------+ +------------+ +------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Glue Job: | | Glue Job: | | Glue Job: |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | dim_date | | dim_system | | dim_planet |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | (WAP) | | (WAP) | | (WAP) |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +------------+ +------------+ +------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +----------------------------+----------------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Wait for all dimensions)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +------------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Glue Job: |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | fact_system_updates |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | (WAP + Incremental) |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +------------------------+
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Data Flow:&lt;/strong>&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Landing Layer&lt;/th>
 &lt;th>Staging Layer (PSA)&lt;/th>
 &lt;th>Semantic Layer&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Raw 1:1 from source&lt;/td>
 &lt;td>Iceberg + data_date partitioning&lt;/td>
 &lt;td>Production dimensional model&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>systems/&lt;/code>&lt;/td>
 &lt;td>&lt;code>stg_systems/&lt;/code>&lt;/td>
 &lt;td>&lt;code>dim_date&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>planets/&lt;/code>&lt;/td>
 &lt;td>&lt;code>stg_planets/&lt;/code>&lt;/td>
 &lt;td>&lt;code>dim_system&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>updates/&lt;/code>&lt;/td>
 &lt;td>&lt;code>stg_updates/&lt;/code>&lt;/td>
 &lt;td>&lt;code>dim_planet&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;/td>
 &lt;td>&lt;/td>
 &lt;td>&lt;code>fact_system_updates&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Let me explain the three layers:&lt;/p>
&lt;p>&lt;strong>Landing Layer&lt;/strong> — Raw data, 1:1 from source systems. No transformations except what&amp;rsquo;s needed to land it. Contains only the &lt;em>latest&lt;/em> fetch. If sourcing fails, this layer fails. Get the data close to you as quickly as possible (into your ecosystem). Typically all the files will be string or varchar to mitigate data type issues from a source system.&lt;/p>
&lt;p>&lt;strong>Staging Layer (Persistent Staging Area)&lt;/strong> — This is where we convert data types and prepare for dimensional modelling. Critically, we partition by &lt;code>data_date&lt;/code>—the date the source data was fetched. This gives us:&lt;/p>
&lt;ul>
&lt;li>Historical reprocessing capability (reload any past date)&lt;/li>
&lt;li>Debugging (what did the data look like on January 15th?)&lt;/li>
&lt;li>Dependency triggers (when a partition is written, trigger downstream jobs)&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Semantic Layer&lt;/strong> — Production-ready dimensional model. This is where business users query. Tables are partitioned appropriately for query performance.&lt;/p>
&lt;p>Also notice the job structure: &lt;strong>each job does its own WAP cycle&lt;/strong>. Dimensions build in parallel, each publishing to main when their audits pass. Fact tables wait for all dimensions to publish, then do their own WAP cycle. This ensures:&lt;/p>
&lt;ul>
&lt;li>Fact tables read from &lt;em>published&lt;/em> dimensions (main branch), not staging branches&lt;/li>
&lt;li>Foreign key lookups use production-ready surrogate keys&lt;/li>
&lt;li>Each job is independently testable&lt;/li>
&lt;/ul>
&lt;h4 id="step-1-setting-up-s3-buckets">Step 1: Setting Up S3 Buckets&lt;/h4>
&lt;p>I&amp;rsquo;m in the S3 console. Click &amp;ldquo;Create bucket&amp;rdquo;. I&amp;rsquo;m calling mine &lt;code>space-data-pipeline-demo&lt;/code>.&lt;/p>
&lt;p>Important settings:&lt;/p>
&lt;ul>
&lt;li>Region: ap-southeast-2 (Sydney—pick one close to you)&lt;/li>
&lt;li>Block all public access: Yes&lt;/li>
&lt;li>Versioning: Enabled&lt;/li>
&lt;li>Default encryption: Enabled&lt;/li>
&lt;/ul>
&lt;p>Inside this bucket:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>space-data-pipeline-demo/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── landing/ ← Raw 1:1 from source (latest only)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ ├── systems/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ ├── planets/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ └── updates/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── staging/ ← PSA with data_date partitioning
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ ├── stg_systems/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ ├── stg_planets/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ └── stg_updates/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└── semantic/ ← Production dimensional model
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── dim_date/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── dim_system/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── dim_planet/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └── fact_system_updates/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-2-creating-the-iceberg-tables">Step 2: Creating the Iceberg Tables&lt;/h4>
&lt;p>Here&amp;rsquo;s the DDL for our dimensional model. Notice the partitioning strategies and SCD Type 2 columns.&lt;/p>
&lt;details>
&lt;summary>&lt;strong>Click to expand: dim_date DDL&lt;/strong>&lt;/summary>
&lt;p>This is a reference dimension—typically loaded once with all dates, then rarely updated. Partitioned by year for query performance.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.dim_date (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> date_key INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> full_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day_of_week STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day_of_month INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day_of_year INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> week_of_year INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month_number INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month_name STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> quarter INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">year&lt;/span> INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> is_weekend STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> is_holiday STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fiscal_quarter INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fiscal_year INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Audit columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_system STRING
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (&lt;span style="color:#66d9ef">year&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/semantic/dim_date/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;details>
&lt;summary>&lt;strong>Click to expand: dim_system DDL (SCD Type 2)&lt;/strong>&lt;/summary>
&lt;p>Tracks history with effective_date/expiration_date. Partitioned by effective_year for efficient history queries.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.dim_system (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> system_key INT, &lt;span style="color:#75715e">-- Surrogate key (generated)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> system_id STRING, &lt;span style="color:#75715e">-- Natural key (from source)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> system_name STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sector STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> main_star_type STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coordinates_x DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coordinates_y DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coordinates_z DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> discovery_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> exploration_status STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- SCD Type 2 columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> effective_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expiration_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_flag STRING, &lt;span style="color:#75715e">-- &amp;#39;Y&amp;#39; or &amp;#39;N&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">-- Audit columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_system STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Partition column
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> effective_year INT
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (effective_year)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/semantic/dim_system/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;details>
&lt;summary>&lt;strong>Click to expand: dim_planet DDL (SCD Type 2)&lt;/strong>&lt;/summary>
&lt;p>Foreign key to dim_system uses the surrogate key. Partitioned by effective_year.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.dim_planet (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> planet_key INT, &lt;span style="color:#75715e">-- Surrogate key
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> planet_id STRING, &lt;span style="color:#75715e">-- Natural key
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> system_key INT, &lt;span style="color:#75715e">-- FK to dim_system (surrogate!)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> planet_name STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> planet_type STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orbital_period DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> has_atmosphere STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> habitability_score DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- SCD Type 2 columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> effective_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expiration_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_flag STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Audit columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_system STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- Partition column
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> effective_year INT
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (effective_year)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/semantic/dim_planet/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;details>
&lt;summary>&lt;strong>Click to expand: fact_system_updates DDL&lt;/strong>&lt;/summary>
&lt;p>Incremental fact table partitioned by date_year. Loaded with delete-then-insert pattern.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.fact_system_updates (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> update_key BIGINT, &lt;span style="color:#75715e">-- Surrogate key
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> date_key INT, &lt;span style="color:#75715e">-- FK to dim_date
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> system_key INT, &lt;span style="color:#75715e">-- FK to dim_system (current surrogate!)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> update_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>, &lt;span style="color:#75715e">-- Original precision preserved
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> status STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ships_present INT, &lt;span style="color:#75715e">-- Additive fact
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> resource_level DOUBLE, &lt;span style="color:#75715e">-- Additive fact
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">-- Audit columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_system STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data_date DATE, &lt;span style="color:#75715e">-- Source data date (for reprocessing)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">-- Partition column
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> date_year INT
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (date_year)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/semantic/fact_system_updates/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;details>
&lt;summary>&lt;strong>Click to expand: Staging Tables DDL (Persistent Staging Area)&lt;/strong>&lt;/summary>
&lt;p>Partitioned by data_date for historical reprocessing.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.stg_systems (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> system_id STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> system_name STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sector STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> main_star_type STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coordinates_x DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coordinates_y DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coordinates_z DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> discovery_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> exploration_status STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- PSA columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> data_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/staging/stg_systems/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (&lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.stg_planets (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> planet_id STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> system_id STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> planet_name STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> planet_type STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> orbital_period DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> has_atmosphere STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> habitability_score DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- PSA columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> data_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/staging/stg_planets/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (&lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> space_exploration.stg_updates (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> system_id STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> update_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> status STRING,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ships_present INT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resource_level DOUBLE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- PSA columns
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> data_date DATE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> load_timestamp &lt;span style="color:#66d9ef">TIMESTAMP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PARTITIONED &lt;span style="color:#66d9ef">BY&lt;/span> (data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LOCATION&lt;/span> &lt;span style="color:#e6db74">&amp;#39;s3://space-data-pipeline-demo/staging/stg_updates/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TBLPROPERTIES (&lt;span style="color:#e6db74">&amp;#39;table_type&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;ICEBERG&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;format&amp;#39;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;parquet&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;p>&lt;strong>Key design decisions:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>dim_date&lt;/strong>: Partitioned by &lt;code>year&lt;/code>. This is a &lt;em>reference dimension&lt;/em>—typically loaded once with all dates from 1900-01-01 to 9999-12-31, then rarely updated.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>dim_system / dim_planet&lt;/strong>: SCD Type 2 with &lt;code>effective_date&lt;/code>, &lt;code>expiration_date&lt;/code>, &lt;code>current_flag&lt;/code>. Partitioned by &lt;code>effective_year&lt;/code> for efficient history queries. Updated daily.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>fact_system_updates&lt;/strong>: Partitioned by &lt;code>date_year&lt;/code>. Loaded incrementally using delete-then-insert pattern.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Staging tables (PSA)&lt;/strong>: Partitioned by &lt;code>data_date&lt;/code>. This lets us reprocess any historical date and provides dependency triggers.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;/br>
&lt;/br>
&lt;h3 id="the-heart-of-the-system-orchestrated-glue-jobs-with-per-table-wap">The Heart of the System: Orchestrated Glue Jobs with Per-Table WAP&lt;/h3>
&lt;hr>
&lt;p>Each job does its own Write-Audit-Publish cycle. Let me walk you through each one.&lt;/p>
&lt;hr>
&lt;h4 id="job-1-build-date-dimension-reference-dimension">Job 1: Build Date Dimension (Reference Dimension)&lt;/h4>
&lt;p>The date dimension is special—it&amp;rsquo;s a &lt;em>reference dimension&lt;/em>. We generate it ourselves with all calendar attributes, typically as a one-time load. You&amp;rsquo;d only rerun this if you need to extend the date range or add new attributes (like new holidays).&lt;/p>
&lt;details>
&lt;summary>&lt;strong>Click to expand: build_dim_date Glue Job&lt;/strong>&lt;/summary>
&lt;p>This job generates a complete date dimension from 2000-01-01 to 2050-12-31 (for demo purposes—production would use a wider range). It creates date attributes like day_of_week, quarter, fiscal_year, and is_weekend, then runs WAP to validate before publishing.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Glue Job: build_dim_date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># This is typically a ONE-TIME RUN to populate all dates&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Rerun only when extending date range or adding attributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> sys
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.transforms &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.utils &lt;span style="color:#f92672">import&lt;/span> getResolvedOptions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.context &lt;span style="color:#f92672">import&lt;/span> SparkContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.context &lt;span style="color:#f92672">import&lt;/span> GlueContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.job &lt;span style="color:#f92672">import&lt;/span> Job
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.functions &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.types &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> datetime &lt;span style="color:#f92672">import&lt;/span> datetime, timedelta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Initialize&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>args &lt;span style="color:#f92672">=&lt;/span> getResolvedOptions(sys&lt;span style="color:#f92672">.&lt;/span>argv, [&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sc &lt;span style="color:#f92672">=&lt;/span> SparkContext()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>glueContext &lt;span style="color:#f92672">=&lt;/span> GlueContext(sc)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark &lt;span style="color:#f92672">=&lt;/span> glueContext&lt;span style="color:#f92672">.&lt;/span>spark_session
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job &lt;span style="color:#f92672">=&lt;/span> Job(glueContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>init(args[&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>], args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;Building Date Dimension (Reference Dimension)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;This is typically a ONE-TIME load&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Generate comprehensive date range&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Standard practice: 1900-01-01 to 9999-12-31&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>start_date &lt;span style="color:#f92672">=&lt;/span> datetime(&lt;span style="color:#ae81ff">1900&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>end_date &lt;span style="color:#f92672">=&lt;/span> datetime(&lt;span style="color:#ae81ff">9999&lt;/span>, &lt;span style="color:#ae81ff">12&lt;/span>, &lt;span style="color:#ae81ff">31&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># For demo purposes, let&amp;#39;s use a more reasonable range&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># In production, you&amp;#39;d use the full range&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>start_date &lt;span style="color:#f92672">=&lt;/span> datetime(&lt;span style="color:#ae81ff">2000&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>end_date &lt;span style="color:#f92672">=&lt;/span> datetime(&lt;span style="color:#ae81ff">2050&lt;/span>, &lt;span style="color:#ae81ff">12&lt;/span>, &lt;span style="color:#ae81ff">31&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Generating dates from &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>start_date&lt;span style="color:#f92672">.&lt;/span>date()&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> to &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>end_date&lt;span style="color:#f92672">.&lt;/span>date()&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date_list &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>current &lt;span style="color:#f92672">=&lt;/span> start_date
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> current &lt;span style="color:#f92672">&amp;lt;=&lt;/span> end_date:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> date_list&lt;span style="color:#f92672">.&lt;/span>append((current,))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current &lt;span style="color:#f92672">+=&lt;/span> timedelta(days&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>createDataFrame(date_list, [&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Build dimension with rich attributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dim_date &lt;span style="color:#f92672">=&lt;/span> date_df&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Surrogate key: YYYYMMDD format&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (year(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">10000&lt;/span> &lt;span style="color:#f92672">+&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">100&lt;/span> &lt;span style="color:#f92672">+&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dayofmonth(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>))&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;int&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> date_format(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;EEEE&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;day_of_week&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dayofmonth(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;day_of_month&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dayofyear(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;day_of_year&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> weekofyear(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;week_of_year&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;month_number&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> date_format(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;MMMM&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;month_name&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> quarter(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;quarter&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> year(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;year&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Business-friendly flags&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> when(dayofweek(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isin([&lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#ae81ff">7&lt;/span>]), &lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>otherwise(&lt;span style="color:#e6db74">&amp;#34;N&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;is_weekend&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;N&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;is_holiday&amp;#34;&lt;/span>), &lt;span style="color:#75715e"># Populate from holiday calendar&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Fiscal calendar (assuming fiscal year = calendar year)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> quarter(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;fiscal_quarter&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> year(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;fiscal_year&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Audit columns&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_timestamp()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;load_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;GENERATED&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;source_system&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>row_count &lt;span style="color:#f92672">=&lt;/span> dim_date&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Generated &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>row_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> date rows&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># WRITE PHASE: Write to staging branch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- WRITE PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;Writing to staging branch...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_date CREATE BRANCH IF NOT EXISTS staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dim_date&lt;span style="color:#f92672">.&lt;/span>writeTo(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_date&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>overwritePartitions()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;Written to staging branch&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># AUDIT PHASE: Validate before publishing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- AUDIT PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staging_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_date&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>audit_failures &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 1: Row count&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staged_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 1 - Row count: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> staged_count &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">&amp;#34;FAIL: dim_date is empty&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 2: Primary key uniqueness&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unique_keys &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> staged_count &lt;span style="color:#f92672">!=&lt;/span> unique_keys:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> duplicates &lt;span style="color:#f92672">=&lt;/span> staged_count &lt;span style="color:#f92672">-&lt;/span> unique_keys
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>duplicates&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> duplicate date_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - PK uniqueness: FAIL (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>duplicates&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> duplicates)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - PK uniqueness: PASS (all &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>unique_keys&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> unique)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 3: No NULLs in critical fields&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>null_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull() &lt;span style="color:#f92672">|&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> null_count &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>null_count&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> NULL values in critical fields&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 3 - NULL check: FAIL (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>null_count&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> NULLs)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 3 - NULL check: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 4: Date range coverage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date_range &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>agg(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> min(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;min_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max(&lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;max_date&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>collect()[&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 4 - Date range: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>date_range[&lt;span style="color:#e6db74">&amp;#39;min_date&amp;#39;&lt;/span>]&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> to &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>date_range[&lt;span style="color:#e6db74">&amp;#39;max_date&amp;#39;&lt;/span>]&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># PUBLISH PHASE: Merge to main if audits pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- PUBLISH PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> len(audit_failures) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">AUDIT FAILED:&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> failure &lt;span style="color:#f92672">in&lt;/span> audit_failures:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34; &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>failure&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">Branch NOT merged. Production unchanged.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_date DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span>(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit failed with &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>len(audit_failures)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> errors&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">ALL AUDITS PASSED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Merging staging branch to main...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Fast-forward main to staging&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> CALL system.fast_forward(
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> table =&amp;gt; &amp;#39;space_exploration.dim_date&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> branch =&amp;gt; &amp;#39;main&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> to =&amp;gt; &amp;#39;staging&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Clean up staging branch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_date DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;PUBLISHED: dim_date (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> rows)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;dim_date job complete&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;hr>
&lt;h4 id="job-2-build-system-dimension-scd-type-2">Job 2: Build System Dimension (SCD Type 2)&lt;/h4>
&lt;p>This dimension tracks history. When a system&amp;rsquo;s attributes change (like &lt;code>exploration_status&lt;/code>), we expire the old row and create a new one.&lt;/p>
&lt;details>
&lt;summary>&lt;strong>Click to expand: build_dim_system Glue Job&lt;/strong>&lt;/summary>
&lt;p>This job implements SCD Type 2 change detection. It compares incoming data against the current dimension, identifies NEW/CHANGED/UNCHANGED records, expires old versions, and creates new versions with updated surrogate keys.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Glue Job: build_dim_system&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># SCD Type 2 implementation with effective/expiration dates&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> sys
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.transforms &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.utils &lt;span style="color:#f92672">import&lt;/span> getResolvedOptions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.context &lt;span style="color:#f92672">import&lt;/span> SparkContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.context &lt;span style="color:#f92672">import&lt;/span> GlueContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.job &lt;span style="color:#f92672">import&lt;/span> Job
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.functions &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.window &lt;span style="color:#f92672">import&lt;/span> Window
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>args &lt;span style="color:#f92672">=&lt;/span> getResolvedOptions(sys&lt;span style="color:#f92672">.&lt;/span>argv, [&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;DATA_DATE&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sc &lt;span style="color:#f92672">=&lt;/span> SparkContext()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>glueContext &lt;span style="color:#f92672">=&lt;/span> GlueContext(sc)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark &lt;span style="color:#f92672">=&lt;/span> glueContext&lt;span style="color:#f92672">.&lt;/span>spark_session
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job &lt;span style="color:#f92672">=&lt;/span> Job(glueContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>init(args[&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>], args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>data_date &lt;span style="color:#f92672">=&lt;/span> args[&lt;span style="color:#e6db74">&amp;#39;DATA_DATE&amp;#39;&lt;/span>] &lt;span style="color:#75715e"># e.g., &amp;#39;2026-01-17&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Building System Dimension (SCD Type 2)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Data Date: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># LOAD: Read from Persistent Staging Area&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- LOAD PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Read today&amp;#39;s staged data (already landed in PSA)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.stg_systems&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_count &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Source rows from PSA (data_date=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">): &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>source_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> source_count &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;No data for this date. Exiting.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sys&lt;span style="color:#f92672">.&lt;/span>exit(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Read current production dimension (for SCD2 comparison)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_dim &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_count &lt;span style="color:#f92672">=&lt;/span> current_dim&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Current dimension rows (current_flag=&amp;#39;Y&amp;#39;): &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>current_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> has_existing_data &lt;span style="color:#f92672">=&lt;/span> current_count &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;No existing dimension data (first load)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> has_existing_data &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_dim &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># TRANSFORM: SCD Type 2 Logic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- TRANSFORM PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">not&lt;/span> has_existing_data:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># First load: all rows are new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;First load - all rows are INSERT&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> window_spec &lt;span style="color:#f92672">=&lt;/span> Window&lt;span style="color:#f92672">.&lt;/span>orderBy(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dim &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> row_number()&lt;span style="color:#f92672">.&lt;/span>over(window_spec)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_name&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;sector&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;main_star_type&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;coordinates_x&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;coordinates_y&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;coordinates_z&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;discovery_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;exploration_status&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(data_date)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;effective_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;9999-12-31&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;expiration_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_timestamp()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;load_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;ELITE_DANGEROUS&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;source_system&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> year(lit(data_date))&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;effective_year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )&lt;span style="color:#f92672">.&lt;/span>distinct()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Incremental load: detect changes for SCD2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Incremental load - detecting changes for SCD2&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Define which columns to track for changes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracked_columns &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;system_name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;sector&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;main_star_type&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;coordinates_x&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;coordinates_y&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;coordinates_z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;exploration_status&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Join source with current to detect changes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> comparison &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;src&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_dim&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;curr&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.system_id&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.system_id&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;full_outer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Identify: NEW (no match in current), CHANGED (values differ), UNCHANGED&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> change_detection &lt;span style="color:#f92672">=&lt;/span> comparison&lt;span style="color:#f92672">.&lt;/span>withColumn(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;change_type&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> when(col(&lt;span style="color:#e6db74">&amp;#34;curr.system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull(), &lt;span style="color:#e6db74">&amp;#34;NEW&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>when(col(&lt;span style="color:#e6db74">&amp;#34;src.system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull(), &lt;span style="color:#e6db74">&amp;#34;DELETED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>when(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (col(&lt;span style="color:#e6db74">&amp;#34;src.system_name&amp;#34;&lt;/span>) &lt;span style="color:#f92672">!=&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.system_name&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (col(&lt;span style="color:#e6db74">&amp;#34;src.sector&amp;#34;&lt;/span>) &lt;span style="color:#f92672">!=&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.sector&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (col(&lt;span style="color:#e6db74">&amp;#34;src.main_star_type&amp;#34;&lt;/span>) &lt;span style="color:#f92672">!=&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.main_star_type&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (col(&lt;span style="color:#e6db74">&amp;#34;src.exploration_status&amp;#34;&lt;/span>) &lt;span style="color:#f92672">!=&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.exploration_status&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (abs(col(&lt;span style="color:#e6db74">&amp;#34;src.coordinates_x&amp;#34;&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.coordinates_x&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0.001&lt;/span>) &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (abs(col(&lt;span style="color:#e6db74">&amp;#34;src.coordinates_y&amp;#34;&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.coordinates_y&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0.001&lt;/span>) &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (abs(col(&lt;span style="color:#e6db74">&amp;#34;src.coordinates_z&amp;#34;&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;curr.coordinates_z&amp;#34;&lt;/span>)) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0.001&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;CHANGED&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>otherwise(&lt;span style="color:#e6db74">&amp;#34;UNCHANGED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Count changes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> change_counts &lt;span style="color:#f92672">=&lt;/span> change_detection&lt;span style="color:#f92672">.&lt;/span>groupBy(&lt;span style="color:#e6db74">&amp;#34;change_type&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>count()&lt;span style="color:#f92672">.&lt;/span>collect()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> row &lt;span style="color:#f92672">in&lt;/span> change_counts:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34; &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>row[&lt;span style="color:#e6db74">&amp;#39;change_type&amp;#39;&lt;/span>]&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>row[&lt;span style="color:#e6db74">&amp;#39;count&amp;#39;&lt;/span>]&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Get max surrogate key for new key generation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_key &lt;span style="color:#f92672">=&lt;/span> current_dim&lt;span style="color:#f92672">.&lt;/span>agg(max(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>))&lt;span style="color:#f92672">.&lt;/span>collect()[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Build new dimension rows for NEW and CHANGED records&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_records &lt;span style="color:#f92672">=&lt;/span> change_detection&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;change_type&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isin([&lt;span style="color:#e6db74">&amp;#34;NEW&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;CHANGED&amp;#34;&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> window_spec &lt;span style="color:#f92672">=&lt;/span> Window&lt;span style="color:#f92672">.&lt;/span>orderBy(&lt;span style="color:#e6db74">&amp;#34;src.system_id&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_rows &lt;span style="color:#f92672">=&lt;/span> new_records&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (row_number()&lt;span style="color:#f92672">.&lt;/span>over(window_spec) &lt;span style="color:#f92672">+&lt;/span> max_key)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.system_name&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;system_name&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.sector&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;sector&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.main_star_type&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;main_star_type&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.coordinates_x&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;coordinates_x&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.coordinates_y&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;coordinates_y&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.coordinates_z&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;coordinates_z&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.discovery_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;discovery_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;src.exploration_status&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;exploration_status&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(data_date)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;effective_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;9999-12-31&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;expiration_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_timestamp()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;load_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;ELITE_DANGEROUS&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;source_system&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> year(lit(data_date))&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;effective_year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Build expired rows (for CHANGED records, expire the old version)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> changed_ids &lt;span style="color:#f92672">=&lt;/span> change_detection&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;change_type&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;CHANGED&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(col(&lt;span style="color:#e6db74">&amp;#34;curr.system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expired_rows &lt;span style="color:#f92672">=&lt;/span> current_dim&lt;span style="color:#f92672">.&lt;/span>join(changed_ids, &lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>withColumn(&lt;span style="color:#e6db74">&amp;#34;expiration_date&amp;#34;&lt;/span>, lit(data_date)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>withColumn(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>, lit(&lt;span style="color:#e6db74">&amp;#34;N&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Unchanged rows stay as-is&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> unchanged_rows &lt;span style="color:#f92672">=&lt;/span> current_dim&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> change_detection&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;change_type&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;UNCHANGED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(col(&lt;span style="color:#e6db74">&amp;#34;curr.system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Historical rows (already expired) - read from full table&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> historical_rows &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;N&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Combine all&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_dim &lt;span style="color:#f92672">=&lt;/span> new_rows \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>unionByName(expired_rows, allowMissingColumns&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>unionByName(unchanged_rows, allowMissingColumns&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>unionByName(historical_rows, allowMissingColumns&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transform_count &lt;span style="color:#f92672">=&lt;/span> new_dim&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Total dimension rows after SCD2: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>transform_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># WRITE PHASE: Write to staging branch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- WRITE PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_system CREATE BRANCH IF NOT EXISTS staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>new_dim&lt;span style="color:#f92672">.&lt;/span>writeTo(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>overwritePartitions()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;Written to staging branch&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># AUDIT PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- AUDIT PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staging_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>audit_failures &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 1: Row count&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staged_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 1 - Row count: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> staged_count &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">&amp;#34;FAIL: dim_system is empty&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 2: Surrogate key uniqueness&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unique_keys &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> staged_count &lt;span style="color:#f92672">!=&lt;/span> unique_keys:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> duplicates &lt;span style="color:#f92672">=&lt;/span> staged_count &lt;span style="color:#f92672">-&lt;/span> unique_keys
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>duplicates&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> duplicate system_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - PK uniqueness: FAIL (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>duplicates&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> duplicates)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - PK uniqueness: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 3: Only one current row per natural key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>current_rows &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>current_count &lt;span style="color:#f92672">=&lt;/span> current_rows&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unique_natural_keys &lt;span style="color:#f92672">=&lt;/span> current_rows&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> current_count &lt;span style="color:#f92672">!=&lt;/span> unique_natural_keys:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: Multiple current rows for same system_id&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 3 - Single current: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 3 - Single current: PASS (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>current_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> current rows)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 4: No NULLs in critical fields&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>null_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull() &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull() &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;effective_date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull() &lt;span style="color:#f92672">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> null_count &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>null_count&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> NULL values in critical fields&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 4 - NULL check: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 4 - NULL check: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 5: Effective/expiration date logic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>invalid_dates &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;effective_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">&amp;gt;&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;expiration_date&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> invalid_dates &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>invalid_dates&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> rows with effective &amp;gt; expiration&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 5 - Date logic: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 5 - Date logic: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># PUBLISH PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- PUBLISH PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> len(audit_failures) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">AUDIT FAILED:&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> failure &lt;span style="color:#f92672">in&lt;/span> audit_failures:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34; &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>failure&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">Branch NOT merged. Production unchanged.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_system DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span>(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit failed with &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>len(audit_failures)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> errors&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">ALL AUDITS PASSED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Merging staging branch to main...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> CALL system.fast_forward(
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> table =&amp;gt; &amp;#39;space_exploration.dim_system&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> branch =&amp;gt; &amp;#39;main&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> to =&amp;gt; &amp;#39;staging&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_system DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;PUBLISHED: dim_system (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> rows)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;hr>
&lt;h4 id="job-3-build-planet-dimension-scd-type-2">Job 3: Build Planet Dimension (SCD Type 2)&lt;/h4>
&lt;p>Similar pattern to dim&lt;em>system, but with a foreign key lookup to the _published&lt;/em> dim_system.&lt;/p>
&lt;details>
&lt;summary>&lt;strong>Click to expand: build_dim_planet Glue Job&lt;/strong>&lt;/summary>
&lt;p>This job demonstrates FK lookup to a published dimension. It reads from the main branch of dim_system (not staging), ensuring it uses production-ready surrogate keys. The FK integrity audit validates this relationship.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Glue Job: build_dim_planet&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># SCD Type 2 with FK lookup to published dim_system&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> sys
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.transforms &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.utils &lt;span style="color:#f92672">import&lt;/span> getResolvedOptions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.context &lt;span style="color:#f92672">import&lt;/span> SparkContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.context &lt;span style="color:#f92672">import&lt;/span> GlueContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.job &lt;span style="color:#f92672">import&lt;/span> Job
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.functions &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.window &lt;span style="color:#f92672">import&lt;/span> Window
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>args &lt;span style="color:#f92672">=&lt;/span> getResolvedOptions(sys&lt;span style="color:#f92672">.&lt;/span>argv, [&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;DATA_DATE&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sc &lt;span style="color:#f92672">=&lt;/span> SparkContext()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>glueContext &lt;span style="color:#f92672">=&lt;/span> GlueContext(sc)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark &lt;span style="color:#f92672">=&lt;/span> glueContext&lt;span style="color:#f92672">.&lt;/span>spark_session
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job &lt;span style="color:#f92672">=&lt;/span> Job(glueContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>init(args[&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>], args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>data_date &lt;span style="color:#f92672">=&lt;/span> args[&lt;span style="color:#e6db74">&amp;#39;DATA_DATE&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Building Planet Dimension (SCD Type 2)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Data Date: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># LOAD PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- LOAD PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.stg_planets&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_count &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Source rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>source_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> source_count &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;No data. Exiting.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sys&lt;span style="color:#f92672">.&lt;/span>exit(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># CRITICAL: Read from PUBLISHED dim_system (main branch, not staging)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># This is why dim_planet must run AFTER dim_system publishes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dim_system &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;dim_system lookup rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>dim_system&lt;span style="color:#f92672">.&lt;/span>count()&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># TRANSFORM PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- TRANSFORM PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Check for existing data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_dim &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_planet&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> has_existing &lt;span style="color:#f92672">=&lt;/span> current_dim&lt;span style="color:#f92672">.&lt;/span>count() &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_key &lt;span style="color:#f92672">=&lt;/span> current_dim&lt;span style="color:#f92672">.&lt;/span>agg(max(&lt;span style="color:#e6db74">&amp;#34;planet_key&amp;#34;&lt;/span>))&lt;span style="color:#f92672">.&lt;/span>collect()[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> has_existing &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_key &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Join source with dim_system to get surrogate key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_with_sk &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>join(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_system,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_df&lt;span style="color:#f92672">.&lt;/span>system_id &lt;span style="color:#f92672">==&lt;/span> dim_system&lt;span style="color:#f92672">.&lt;/span>system_id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;left&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_df[&lt;span style="color:#e6db74">&amp;#34;*&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_system&lt;span style="color:#f92672">.&lt;/span>system_key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Track orphaned planets (no matching system)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orphaned &lt;span style="color:#f92672">=&lt;/span> source_with_sk&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull())&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> orphaned &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;WARNING: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>orphaned&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> planets have no matching system!&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># SCD2 logic (simplified for first load demonstration)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>window_spec &lt;span style="color:#f92672">=&lt;/span> Window&lt;span style="color:#f92672">.&lt;/span>orderBy(&lt;span style="color:#e6db74">&amp;#34;planet_id&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>new_dim &lt;span style="color:#f92672">=&lt;/span> source_with_sk&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (row_number()&lt;span style="color:#f92672">.&lt;/span>over(window_spec) &lt;span style="color:#f92672">+&lt;/span> max_key)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;planet_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;planet_id&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;planet_name&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;planet_type&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;orbital_period&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;has_atmosphere&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;habitability_score&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(data_date)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;effective_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;9999-12-31&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;expiration_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_timestamp()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;load_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;ELITE_DANGEROUS&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;source_system&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> year(lit(data_date))&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;effective_year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>distinct()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># (Full SCD2 change detection would follow same pattern as dim_system)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transform_count &lt;span style="color:#f92672">=&lt;/span> new_dim&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Dimension rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>transform_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># WRITE PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- WRITE PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_planet CREATE BRANCH IF NOT EXISTS staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>new_dim&lt;span style="color:#f92672">.&lt;/span>writeTo(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_planet&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>overwritePartitions()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;Written to staging branch&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># AUDIT PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- AUDIT PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staging_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_planet&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>audit_failures &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 1: PK uniqueness&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staged_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unique_keys &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;planet_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> staged_count &lt;span style="color:#f92672">!=&lt;/span> unique_keys:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: Duplicate planet_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 1 - PK uniqueness: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 2: FK integrity to dim_system (published!)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>published_system_keys &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orphaned_fk &lt;span style="color:#f92672">=&lt;/span> staging_df \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNotNull()) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct() \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(published_system_keys, &lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;left_anti&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> orphaned_fk &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>orphaned_fk&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> orphaned system_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - FK integrity: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 2 - FK integrity: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 3: NULL check&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>null_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;planet_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull() &lt;span style="color:#f92672">|&lt;/span> col(&lt;span style="color:#e6db74">&amp;#34;planet_id&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> null_count &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>null_count&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> NULLs in critical fields&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 3 - NULL check: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># PUBLISH PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- PUBLISH PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> len(audit_failures) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">AUDIT FAILED:&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> f &lt;span style="color:#f92672">in&lt;/span> audit_failures:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34; &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>f&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_planet DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span>(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit failed&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">ALL AUDITS PASSED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> CALL system.fast_forward(
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> table =&amp;gt; &amp;#39;space_exploration.dim_planet&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> branch =&amp;gt; &amp;#39;main&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> to =&amp;gt; &amp;#39;staging&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.dim_planet DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;PUBLISHED: dim_planet (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> rows)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;hr>
&lt;h4 id="job-4-build-fact-table-incremental-with-delete-insert">Job 4: Build Fact Table (Incremental with Delete-Insert)&lt;/h4>
&lt;p>This is the critical job. It reads from &lt;em>published&lt;/em> dimensions (main branch), uses delete-then-insert pattern for the date partition, and includes left anti-join as a defensive check for missing records.&lt;/p>
&lt;details>
&lt;summary>&lt;strong>Click to expand: build_fact_system_updates Glue Job&lt;/strong>&lt;/summary>
&lt;p>This job loads facts incrementally by date. For a given data_date, it deletes existing records and inserts new ones. It validates FK integrity against all published dimensions before merging to main.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Glue Job: build_fact_system_updates&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Incremental load with delete-then-insert + left anti-join defensive coding&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> sys
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.transforms &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.utils &lt;span style="color:#f92672">import&lt;/span> getResolvedOptions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.context &lt;span style="color:#f92672">import&lt;/span> SparkContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.context &lt;span style="color:#f92672">import&lt;/span> GlueContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> awsglue.job &lt;span style="color:#f92672">import&lt;/span> Job
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.functions &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#f92672">*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> pyspark.sql.window &lt;span style="color:#f92672">import&lt;/span> Window
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>args &lt;span style="color:#f92672">=&lt;/span> getResolvedOptions(sys&lt;span style="color:#f92672">.&lt;/span>argv, [&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;DATA_DATE&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sc &lt;span style="color:#f92672">=&lt;/span> SparkContext()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>glueContext &lt;span style="color:#f92672">=&lt;/span> GlueContext(sc)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark &lt;span style="color:#f92672">=&lt;/span> glueContext&lt;span style="color:#f92672">.&lt;/span>spark_session
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job &lt;span style="color:#f92672">=&lt;/span> Job(glueContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>init(args[&lt;span style="color:#e6db74">&amp;#39;JOB_NAME&amp;#39;&lt;/span>], args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>data_date &lt;span style="color:#f92672">=&lt;/span> args[&lt;span style="color:#e6db74">&amp;#39;DATA_DATE&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Building Fact Table: System Updates (Incremental)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Data Date: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;=&amp;#34;&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">60&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># LOAD PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- LOAD PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Read from PSA for this data_date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.stg_updates&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_count &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Source rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>source_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> source_count &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;No data. Exiting.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sys&lt;span style="color:#f92672">.&lt;/span>exit(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Read PUBLISHED dimensions (main branch)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Fact tables MUST read from published dimensions, not staging branches&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dim_system &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;current_flag&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Y&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;system_id&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dim_date &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_date&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;full_date&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;dim_system rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>dim_system&lt;span style="color:#f92672">.&lt;/span>count()&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;dim_date rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>dim_date&lt;span style="color:#f92672">.&lt;/span>count()&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Read existing fact table for incremental logic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> existing_fact &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.fact_system_updates&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> existing_count &lt;span style="color:#f92672">=&lt;/span> existing_fact&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Existing fact rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>existing_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_key &lt;span style="color:#f92672">=&lt;/span> existing_fact&lt;span style="color:#f92672">.&lt;/span>agg(max(&lt;span style="color:#e6db74">&amp;#34;update_key&amp;#34;&lt;/span>))&lt;span style="color:#f92672">.&lt;/span>collect()[&lt;span style="color:#ae81ff">0&lt;/span>][&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;No existing fact data (first load)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> existing_fact &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> max_key &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># TRANSFORM PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- TRANSFORM PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Add date column for joining&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source_with_date &lt;span style="color:#f92672">=&lt;/span> source_df&lt;span style="color:#f92672">.&lt;/span>withColumn(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;update_date&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> to_date(&lt;span style="color:#e6db74">&amp;#34;update_timestamp&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Lookup surrogate keys from published dimensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>fact_transformed &lt;span style="color:#f92672">=&lt;/span> source_with_date \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(dim_system, source_with_date&lt;span style="color:#f92672">.&lt;/span>system_id &lt;span style="color:#f92672">==&lt;/span> dim_system&lt;span style="color:#f92672">.&lt;/span>system_id, &lt;span style="color:#e6db74">&amp;#34;left&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(dim_date, source_with_date&lt;span style="color:#f92672">.&lt;/span>update_date &lt;span style="color:#f92672">==&lt;/span> dim_date&lt;span style="color:#f92672">.&lt;/span>full_date, &lt;span style="color:#e6db74">&amp;#34;left&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_system&lt;span style="color:#f92672">.&lt;/span>system_key,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> source_with_date&lt;span style="color:#f92672">.&lt;/span>update_timestamp,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;status&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;ships_present&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;resource_level&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(data_date)&lt;span style="color:#f92672">.&lt;/span>cast(&lt;span style="color:#e6db74">&amp;#34;date&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dim_date&lt;span style="color:#f92672">.&lt;/span>year&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;date_year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Track orphaned facts&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orphaned_systems &lt;span style="color:#f92672">=&lt;/span> fact_transformed&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull())&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orphaned_dates &lt;span style="color:#f92672">=&lt;/span> fact_transformed&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull())&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> orphaned_systems &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;WARNING: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>orphaned_systems&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> facts have no matching system&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> orphaned_dates &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;WARNING: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>orphaned_dates&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> facts have no matching date&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># INCREMENTAL LOGIC: Delete-then-Insert + Left Anti-Join&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- INCREMENTAL LOGIC ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Extract the date partition we&amp;#39;re loading&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date_partition_year &lt;span style="color:#f92672">=&lt;/span> int(data_date[:&lt;span style="color:#ae81ff">4&lt;/span>]) &lt;span style="color:#75715e"># e.g., 2026&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> existing_fact &lt;span style="color:#f92672">is&lt;/span> &lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#66d9ef">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Step 1: Get existing records for this data_date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> existing_for_date &lt;span style="color:#f92672">=&lt;/span> existing_fact&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> existing_for_date_count &lt;span style="color:#f92672">=&lt;/span> existing_for_date&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Existing records for data_date=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>existing_for_date_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Step 2: Records NOT in this date (keep as-is)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> records_to_keep &lt;span style="color:#f92672">=&lt;/span> existing_fact&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">!=&lt;/span> data_date)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> keep_count &lt;span style="color:#f92672">=&lt;/span> records_to_keep&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Records to keep (other dates): &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>keep_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Step 3: New records from source (delete-then-insert for this date)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Generate new surrogate keys starting after max existing key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> window_spec &lt;span style="color:#f92672">=&lt;/span> Window&lt;span style="color:#f92672">.&lt;/span>orderBy(&lt;span style="color:#e6db74">&amp;#34;update_timestamp&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_records &lt;span style="color:#f92672">=&lt;/span> fact_transformed&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (row_number()&lt;span style="color:#f92672">.&lt;/span>over(window_spec) &lt;span style="color:#f92672">+&lt;/span> max_key)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;update_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;update_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;status&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;ships_present&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;resource_level&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_timestamp()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;load_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;ELITE_DANGEROUS&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;source_system&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;date_year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> new_count &lt;span style="color:#f92672">=&lt;/span> new_records&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;New records for data_date=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>new_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Step 4: Left anti-join defensive check&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Find any records in destination that should exist but don&amp;#39;t&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># (This catches edge cases where source has records not in our transform)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># For this example, we&amp;#39;re doing a full replace for the date, so this is informational&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Step 5: Combine: kept records + new records (effectively delete-insert)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> final_fact &lt;span style="color:#f92672">=&lt;/span> records_to_keep&lt;span style="color:#f92672">.&lt;/span>unionByName(new_records)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># First load: all records are new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> window_spec &lt;span style="color:#f92672">=&lt;/span> Window&lt;span style="color:#f92672">.&lt;/span>orderBy(&lt;span style="color:#e6db74">&amp;#34;update_timestamp&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> final_fact &lt;span style="color:#f92672">=&lt;/span> fact_transformed&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> row_number()&lt;span style="color:#f92672">.&lt;/span>over(window_spec)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;update_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;update_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;status&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;ships_present&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;resource_level&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current_timestamp()&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;load_timestamp&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lit(&lt;span style="color:#e6db74">&amp;#34;ELITE_DANGEROUS&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>alias(&lt;span style="color:#e6db74">&amp;#34;source_system&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> col(&lt;span style="color:#e6db74">&amp;#34;date_year&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transform_count &lt;span style="color:#f92672">=&lt;/span> final_fact&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Final fact rows: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>transform_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># WRITE PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- WRITE PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.fact_system_updates CREATE BRANCH IF NOT EXISTS staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>final_fact&lt;span style="color:#f92672">.&lt;/span>writeTo(&lt;span style="color:#e6db74">&amp;#34;space_exploration.fact_system_updates&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>overwritePartitions()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;Written to staging branch&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># AUDIT PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- AUDIT PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staging_df &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>option(&lt;span style="color:#e6db74">&amp;#34;branch&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;staging&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.fact_system_updates&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>audit_failures &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 1: Row count (source to target for this date)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staged_for_date &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;data_date&amp;#34;&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> data_date)&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 1 - Row count for &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">: Source=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>source_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">, Target=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_for_date&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> source_count &lt;span style="color:#f92672">!=&lt;/span> staged_for_date:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># This might be OK if we filtered orphans, but flag for review&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34; Note: Difference of &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>source_count &lt;span style="color:#f92672">-&lt;/span> staged_for_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> (likely orphaned records)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 2: PK uniqueness&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>staged_count &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unique_keys &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;update_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> staged_count &lt;span style="color:#f92672">!=&lt;/span> unique_keys:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: Duplicate update_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - PK uniqueness: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 2 - PK uniqueness: PASS (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> unique)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 3: FK integrity to dim_system&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>published_system_keys &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_system&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orphaned_fk_system &lt;span style="color:#f92672">=&lt;/span> staging_df \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNotNull()) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct() \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(published_system_keys, &lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;left_anti&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> orphaned_fk_system &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>orphaned_fk_system&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> orphaned system_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 3 - FK to dim_system: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 3 - FK to dim_system: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 4: FK integrity to dim_date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>published_date_keys &lt;span style="color:#f92672">=&lt;/span> spark&lt;span style="color:#f92672">.&lt;/span>read&lt;span style="color:#f92672">.&lt;/span>table(&lt;span style="color:#e6db74">&amp;#34;space_exploration.dim_date&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>orphaned_fk_date &lt;span style="color:#f92672">=&lt;/span> staging_df \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNotNull()) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>select(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>distinct() \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join(published_date_keys, &lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;left_anti&amp;#34;&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> orphaned_fk_date &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>orphaned_fk_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> orphaned date_keys&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 4 - FK to dim_date: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 4 - FK to dim_date: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 5: No NULLs in mandatory FKs&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># (We allow NULLs for orphaned records, but track them)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>null_system &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;system_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull())&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>null_date &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;date_key&amp;#34;&lt;/span>)&lt;span style="color:#f92672">.&lt;/span>isNull())&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 5 - NULL FKs: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>null_system&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> null system_key, &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>null_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> null date_key&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 6: Value range checks&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>invalid_resources &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (col(&lt;span style="color:#e6db74">&amp;#34;resource_level&amp;#34;&lt;/span>) &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>) &lt;span style="color:#f92672">|&lt;/span> (col(&lt;span style="color:#e6db74">&amp;#34;resource_level&amp;#34;&lt;/span>) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> invalid_resources &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>invalid_resources&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> invalid resource_level values&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 6 - Value ranges: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 6 - Value ranges: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Audit 7: No future timestamps&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>future_ts &lt;span style="color:#f92672">=&lt;/span> staging_df&lt;span style="color:#f92672">.&lt;/span>filter(col(&lt;span style="color:#e6db74">&amp;#34;update_timestamp&amp;#34;&lt;/span>) &lt;span style="color:#f92672">&amp;gt;&lt;/span> current_timestamp())&lt;span style="color:#f92672">.&lt;/span>count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> future_ts &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> audit_failures&lt;span style="color:#f92672">.&lt;/span>append(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;FAIL: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>future_ts&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> future timestamps&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit 7 - Timestamps: FAIL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Audit 7 - Timestamps: PASS&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># PUBLISH PHASE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># ============================================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">--- PUBLISH PHASE ---&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> len(audit_failures) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">AUDIT FAILED:&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> f &lt;span style="color:#f92672">in&lt;/span> audit_failures:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34; &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>f&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.fact_system_updates DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span>(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Audit failed with &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>len(audit_failures)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> errors&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">ALL AUDITS PASSED&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> CALL system.fast_forward(
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> table =&amp;gt; &amp;#39;space_exploration.fact_system_updates&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> branch =&amp;gt; &amp;#39;main&amp;#39;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> to =&amp;gt; &amp;#39;staging&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> spark&lt;span style="color:#f92672">.&lt;/span>sql(&lt;span style="color:#e6db74">&amp;#34;ALTER TABLE space_exploration.fact_system_updates DROP BRANCH staging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;PUBLISHED: fact_system_updates (&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>staged_count&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">,&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> rows)&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>job&lt;span style="color:#f92672">.&lt;/span>commit()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;hr>
&lt;h3 id="step-functions-workflow">Step Functions Workflow&lt;/h3>
&lt;hr>
&lt;p>The Step Functions workflow orchestrates everything. Here&amp;rsquo;s the execution flow:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Stage 1 (Parallel):&lt;/strong> dim_date and dim_system run simultaneously—neither depends on the other&lt;/li>
&lt;li>&lt;strong>Stage 2:&lt;/strong> dim_planet runs after Stage 1 completes (needs dim_system&amp;rsquo;s surrogate keys)&lt;/li>
&lt;li>&lt;strong>Stage 3:&lt;/strong> fact_system_updates runs last (needs all dimension surrogate keys)&lt;/li>
&lt;/ol>
&lt;p>If any job fails, the workflow stops and sends an SNS notification.&lt;/p>
&lt;details>
&lt;summary>&lt;strong>Click to expand: Step Functions JSON Definition&lt;/strong>&lt;/summary>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Comment&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Dimensional Modeling Pipeline - Per-Table WAP Pattern&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;StartAt&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Parallel Dimensions Stage 1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;States&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Parallel Dimensions Stage 1&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Parallel&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Comment&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;dim_date and dim_system have no dependencies&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Branches&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;StartAt&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Build dim_date (WAP)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;States&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Build dim_date (WAP)&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Task&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Resource&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;arn:aws:states:::glue:startJobRun.sync&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Parameters&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;JobName&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;build_dim_date&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;End&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;StartAt&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Build dim_system (WAP)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;States&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Build dim_system (WAP)&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Task&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Resource&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;arn:aws:states:::glue:startJobRun.sync&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Parameters&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;JobName&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;build_dim_system&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Arguments&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;--DATA_DATE.$&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;$.data_date&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;End&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Build dim_planet (WAP)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Catch&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;ErrorEquals&amp;#34;&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;States.ALL&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Notify Failure&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;ResultPath&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;$.error&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Build dim_planet (WAP)&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Task&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Comment&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Depends on dim_system being published&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Resource&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;arn:aws:states:::glue:startJobRun.sync&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Parameters&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;JobName&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;build_dim_planet&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Arguments&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;--DATA_DATE.$&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;$.data_date&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Build fact_system_updates (WAP)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Catch&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;ErrorEquals&amp;#34;&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;States.ALL&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Notify Failure&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;ResultPath&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;$.error&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Build fact_system_updates (WAP)&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Task&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Comment&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Depends on ALL dimensions being published&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Resource&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;arn:aws:states:::glue:startJobRun.sync&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Parameters&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;JobName&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;build_fact_system_updates&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Arguments&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;--DATA_DATE.$&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;$.data_date&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Pipeline Succeeded&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Catch&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;ErrorEquals&amp;#34;&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;States.ALL&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Notify Failure&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;ResultPath&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;$.error&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Notify Failure&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Task&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Resource&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;arn:aws:states:::sns:publish&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Parameters&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;TopicArn&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;arn:aws:sns:ap-southeast-2:ACCOUNT:pipeline-alerts&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Message.$&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;States.Format(&amp;#39;Pipeline failed: {}&amp;#39;, $.error.Cause)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Subject&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Data Pipeline Failed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Next&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Pipeline Failed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Pipeline Succeeded&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Succeed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Pipeline Failed&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Fail&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Error&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;PipelineError&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Cause&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;One or more jobs failed - check individual job logs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;p>Start the workflow with input:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;data_date&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;2026-01-17&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This &lt;code>data_date&lt;/code> propagates to each job, enabling:&lt;/p>
&lt;ul>
&lt;li>PSA partition reads (&lt;code>WHERE data_date = '2026-01-17'&lt;/code>)&lt;/li>
&lt;li>Historical reprocessing (re-run with a past date)&lt;/li>
&lt;li>Dependency triggers (S3 event on partition write triggers workflow)&lt;/li>
&lt;/ul>
&lt;/br>
&lt;/br>
&lt;h3 id="dependency-triggers-with-s3-events">Dependency Triggers with S3 Events&lt;/h3>
&lt;hr>
&lt;p>You can improve this further by using partition writes as triggers. Here&amp;rsquo;s how to set that up:&lt;/p>
&lt;p>&lt;strong>Option 1: S3 Event → EventBridge → Step Functions&lt;/strong>&lt;/p>
&lt;p>When a file lands in &lt;code>landing/updates/&lt;/code>, trigger the pipeline:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># EventBridge Rule&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;source&amp;#34;: &lt;/span>[&lt;span style="color:#e6db74">&amp;#34;aws.s3&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;detail-type&amp;#34;: &lt;/span>[&lt;span style="color:#e6db74">&amp;#34;Object Created&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;detail&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;bucket&amp;#34;: { &amp;#34;name&amp;#34;: &lt;/span>[&lt;span style="color:#e6db74">&amp;#34;space-data-pipeline-demo&amp;#34;&lt;/span>] },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;object&amp;#34;: { &amp;#34;key&amp;#34;: [{ &amp;#34;prefix&amp;#34;: &lt;/span>&lt;span style="color:#e6db74">&amp;#34;landing/updates/&amp;#34;&lt;/span> }] },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Option 2: Glue Trigger on Partition&lt;/strong>&lt;/p>
&lt;p>Configure Glue to watch for new partitions:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Check if partition exists before running&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>partition_path &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;s3://space-data-pipeline-demo/staging/stg_updates/data_date=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> boto3
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>s3 &lt;span style="color:#f92672">=&lt;/span> boto3&lt;span style="color:#f92672">.&lt;/span>client(&lt;span style="color:#e6db74">&amp;#39;s3&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s3&lt;span style="color:#f92672">.&lt;/span>head_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Bucket&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#39;space-data-pipeline-demo&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;staging/stg_updates/data_date=&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>data_date&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/_SUCCESS&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Partition exists - proceeding&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Partition not ready - exiting&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sys&lt;span style="color:#f92672">.&lt;/span>exit(&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This lets you build dependency chains based on partition availability.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h3 id="the-moment-of-truth-querying">The Moment of Truth: Querying&lt;/h3>
&lt;hr>
&lt;p>Now let&amp;rsquo;s query the properly partitioned dimensional model:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Query leverages date partition pruning
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dt.&lt;span style="color:#66d9ef">year&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dt.quarter,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d.main_star_type,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">COUNT&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>) &lt;span style="color:#66d9ef">as&lt;/span> updates,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SUM&lt;/span>(f.ships_present) &lt;span style="color:#66d9ef">as&lt;/span> total_ships,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ROUND(&lt;span style="color:#66d9ef">AVG&lt;/span>(f.resource_level), &lt;span style="color:#ae81ff">3&lt;/span>) &lt;span style="color:#66d9ef">as&lt;/span> avg_resources
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> space_exploration.fact_system_updates f
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> space_exploration.dim_system d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ON&lt;/span> f.system_key &lt;span style="color:#f92672">=&lt;/span> d.system_key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> d.current_flag &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;Y&amp;#39;&lt;/span> &lt;span style="color:#75715e">-- Only current dimension records!
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> space_exploration.dim_date dt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">ON&lt;/span> f.date_key &lt;span style="color:#f92672">=&lt;/span> dt.date_key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dt.&lt;span style="color:#66d9ef">year&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">2026&lt;/span> &lt;span style="color:#75715e">-- Partition pruning!
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">AND&lt;/span> dt.quarter &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">GROUP&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> dt.&lt;span style="color:#66d9ef">year&lt;/span>, dt.quarter, d.main_star_type
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_ships &lt;span style="color:#66d9ef">DESC&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Notice &lt;code>d.current_flag = 'Y'&lt;/code> — this is how you query SCD Type 2 dimensions for current state. For point-in-time analysis:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Point-in-time analysis: What was system status on Jan 15, 2026?
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d.system_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d.exploration_status,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d.effective_date,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d.expiration_date
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> space_exploration.dim_system d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> d.system_id &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;SOL&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">AND&lt;/span> DATE &lt;span style="color:#e6db74">&amp;#39;2026-01-15&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">BETWEEN&lt;/span> d.effective_date &lt;span style="color:#66d9ef">AND&lt;/span> d.expiration_date;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/br>
&lt;/br>
&lt;h3 id="a-few-things-worth-remembering">A Few Things Worth Remembering&lt;/h3>
&lt;hr>
&lt;p>&lt;strong>WAP at each table, not just at the end.&lt;/strong> Each job should do its own Write-Audit-Publish cycle. Failures are isolated. Downstream jobs read from published (main) tables.&lt;/p>
&lt;p>&lt;strong>Landing is raw, Staging is persistent.&lt;/strong> Landing = 1:1 from source, latest only. Staging (PSA) = partitioned by &lt;code>data_date&lt;/code> for historical reprocessing.&lt;/p>
&lt;p>&lt;strong>dim_date is a reference dimension.&lt;/strong> Load it once with a wide date range. Partition by year. Rarely needs updates.&lt;/p>
&lt;p>&lt;strong>SCD Type 2 for dimensions that change.&lt;/strong> Add &lt;code>effective_date&lt;/code>, &lt;code>expiration_date&lt;/code>, &lt;code>current_flag&lt;/code>. Partition by effective year. Query with &lt;code>current_flag = 'Y'&lt;/code> for current state.&lt;/p>
&lt;p>&lt;strong>Incremental facts with delete-then-insert.&lt;/strong> For a given date partition: delete existing, insert new. Use left anti-join as defensive check for missing records.&lt;/p>
&lt;p>&lt;strong>Partition strategically:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>dim_date: by year&lt;/li>
&lt;li>dim_system/planet: by effective_year (SCD2)&lt;/li>
&lt;li>fact: by date_year&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Use data_date for dependencies.&lt;/strong> When &lt;code>stg_updates/data_date=2026-01-17/&lt;/code> is written, trigger the downstream pipeline. This makes dependencies explicit and reprocessing easy.&lt;/p>
&lt;hr>
&lt;p>That ten-minute query? Down to 1.2 seconds with partition pruning.&lt;/p>
&lt;p>That conference room moment? Each job&amp;rsquo;s WAP cycle catches issues before they propagate.&lt;/p>
&lt;p>These patterns work. They&amp;rsquo;ve saved me from embarrassment, late nights, and angry emails. They&amp;rsquo;ll do the same for you.&lt;/p></content:encoded><category>Data Engineering</category><category>Platforms &amp; Cloud</category><category>AWS Glue</category><category>Dimensional Modelling</category><category>Kimball</category><category>Data Quality</category><category>ETL</category><category>Write-Audit-Publish</category><category>Apache Iceberg</category><category>Step Functions</category><category>SCD</category></item><item><title>Why Dimensional Modelling Isn't Dead—It's Just Getting Started</title><link>https://ghostinthedata.info/posts/2025/2025-11-07-effective-data-modelling/</link><pubDate>Fri, 07 Nov 2025 09:00:00 +1100</pubDate><guid>https://ghostinthedata.info/posts/2025/2025-11-07-effective-data-modelling/</guid><author>Chris Hillman</author><description>Despite claims that big data killed traditional data warehousing, dimensional modelling is more relevant than ever. Here's the evidence from companies managing petabytes and the real costs of skipping proper data modelling.</description><content:encoded>&lt;h2 id="the-great-data-modelling-debate-nobody-asked-for">The Great Data Modelling Debate Nobody Asked For&lt;/h2>
&lt;hr>
&lt;p>Another meeting where someone confidently declared, &amp;ldquo;We don&amp;rsquo;t need data modelling anymore—just dump everything in the data lake and let analysts figure it out.&amp;rdquo;&lt;/p>
&lt;p>I&amp;rsquo;ve heard variations of this statement for years now, in meetings or at conferences. The pitch is always the same: traditional data warehousing is dead, dimensional modelling is a relic from the 90s, and modern big data tools have made structured modelling obsolete. Schema-on-read is the future. Agility over architecture.&lt;/p>
&lt;p>You know what&amp;rsquo;s fascinating? The people making these arguments usually end up drowning in a data swamp they can&amp;rsquo;t navigate.&lt;/p>
&lt;p>Here&amp;rsquo;s the thing nobody wants to admit: &lt;strong>dimensional modelling isn&amp;rsquo;t dead—it&amp;rsquo;s more widely used than at any other point in history.&lt;/strong> Ralph Kimball&amp;rsquo;s methodology from 1996 now powers over 90% of enterprise data warehouses, delivering measurable ROI of 401% over three years. Companies from Uber (managing 100+ petabytes) to Netflix, Airbnb, and Spotify continue betting on dimensional approaches because they deliver what actually matters: fast queries, consistent metrics, and analytics that business users can understand.&lt;/p>
&lt;p>The debate isn&amp;rsquo;t whether to model data. It&amp;rsquo;s how to adapt proven principles to modern platforms.&lt;/p>
&lt;p>Let me show you why.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-fundamentals-that-refuse-to-die">The Fundamentals That Refuse to Die&lt;/h2>
&lt;hr>
&lt;p>Ralph Kimball introduced dimensional modelling to the data warehousing world in 1996 with &amp;ldquo;The Data Warehouse Toolkit,&amp;rdquo; which became the definitive guide for organising analytical data. The methodology centres on a deceptively simple four-step process: select the business process, declare the grain, identify the dimensions, and identify the facts.&lt;/p>
&lt;p>This framework emerged from Kimball&amp;rsquo;s background in human-computer interaction at Xerox PARC, where he helped design the Star Workstation—the first commercial product using mice, icons, and windows. His insight wasn&amp;rsquo;t just about database structure. It was about how humans naturally think about business measurements.&lt;/p>
&lt;p>The brilliance lies in a fundamental division: &lt;strong>separate measurements from context.&lt;/strong> Facts are numeric measurements taken repeatedly from business processes—sales amounts, quantities, durations. Dimensions provide the &amp;ldquo;who, what, where, when, why, and how&amp;rdquo; context surrounding those measurements. This creates star schemas where a central fact table connects to surrounding dimension tables, resembling a star when visualised.&lt;/p>
&lt;p>The denormalised structure deliberately relaxes traditional database normalisation rules to optimise for analytical queries rather than transactional updates. And that&amp;rsquo;s where the magic happens.&lt;/p>
&lt;p>&lt;strong>Star schemas deliver concrete performance advantages.&lt;/strong> According to the official Kimball methodology documentation, the benefits include simpler queries compared to highly normalised schemas, simplified business reporting logic, query performance gains for read-only applications, and fast aggregations. The flat, denormalised dimension tables are perfect targets for bitmap indexes, while star-join optimisations built into modern database engines can process these patterns efficiently.&lt;/p>
&lt;p>Kimball explicitly warns against snowflaking dimensions into third normal form because it &amp;ldquo;destroys the ability to use bitmap indexes and increases the user-perceived complexity of the design.&amp;rdquo; When you&amp;rsquo;re trying to answer business questions quickly, that complexity tax adds up fast.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-integration-secret-conformed-dimensions">The Integration Secret: Conformed Dimensions&lt;/h2>
&lt;hr>
&lt;p>Here&amp;rsquo;s where dimensional modelling gets really powerful—and where most data lakes completely fall apart.&lt;/p>
&lt;p>&lt;strong>Conformed dimensions&lt;/strong> represent the integration backbone of enterprise data warehousing. When dimension tables share identical column names and domain contents across different fact tables, they enable &amp;ldquo;drilling across&amp;rdquo;—combining information from separate business processes in unified reports.&lt;/p>
&lt;p>Think about it this way: a date dimension conforms across sales, inventory, and customer service. A product dimension spans sales, procurement, and manufacturing. As the Kimball Group states: &amp;ldquo;This is the essence of integration in an enterprise DW/BI system. Conformed dimensions, defined once in collaboration with the business&amp;rsquo;s data governance representatives, are reused across fact tables; they deliver both analytic consistency and reduced future development costs because the wheel is not repeatedly re-created.&amp;rdquo;&lt;/p>
&lt;p>I can&amp;rsquo;t tell you how many teams I&amp;rsquo;ve seen struggle because they have five different &amp;ldquo;customer&amp;rdquo; definitions, three different ways to calculate &amp;ldquo;revenue,&amp;rdquo; and no standard date dimension. Every analyst builds their own version. Every report uses slightly different logic. Nobody trusts the numbers because they never match.&lt;/p>
&lt;p>Conformed dimensions solve this. Once. Properly. Forever.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="slowly-changing-dimensions-the-time-machine-nobody-talks-about">Slowly Changing Dimensions: The Time Machine Nobody Talks About&lt;/h2>
&lt;hr>
&lt;p>One of the most elegant solutions in dimensional modelling addresses a tricky problem: how do you track attributes that change over time?&lt;/p>
&lt;p>&lt;strong>Slowly Changing Dimensions (SCDs)&lt;/strong> handle this challenge through several type patterns:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Type 0&lt;/strong>: Preserves original values that never change&lt;/li>
&lt;li>&lt;strong>Type 1&lt;/strong>: Overwrites old values, destroying history but remaining simple&lt;/li>
&lt;li>&lt;strong>Type 2&lt;/strong>: Adds new rows with new surrogate keys whenever attributes change, preserving complete historical accuracy through row effective dates, expiration dates, and current row indicators&lt;/li>
&lt;li>&lt;strong>Type 3&lt;/strong>: Adds columns for alternate reality comparisons&lt;/li>
&lt;li>&lt;strong>Types 4-7&lt;/strong>: Hybrid approaches for rapidly changing attributes or dual historical/current perspectives&lt;/li>
&lt;/ul>
&lt;p>Type 2 is the most common approach, and for good reason. It enables time-variant analysis while maintaining referential integrity between facts and dimensions. You can answer questions like &amp;ldquo;what was this customer&amp;rsquo;s address when they placed that order three years ago?&amp;rdquo; without complex joins or temporal logic in every query.&lt;/p>
&lt;p>Try doing that efficiently in a schema-on-read data lake without proper modelling. I&amp;rsquo;ll wait.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-performance-numbers-that-matter">The Performance Numbers That Matter&lt;/h2>
&lt;hr>
&lt;p>Let&amp;rsquo;s talk about actual performance, because that&amp;rsquo;s where the rubber meets the road.&lt;/p>
&lt;p>Apache Doris demonstrated &lt;strong>31x performance improvement&lt;/strong> on Star Schema Benchmark (SSB) queries between versions. StarRocks proved &lt;strong>4.75x faster than Apache Druid&lt;/strong> and &lt;strong>1.87x faster than ClickHouse&lt;/strong> on 100GB datasets. Most queries complete under 200 milliseconds, with Apache Kylin achieving O(1) constant-time query latency regardless of data size.&lt;/p>
&lt;p>Academic research on distributed dimensional data warehouses shows materialised views can reduce query processing time by &lt;strong>89-98% with only 13% disk space overhead&lt;/strong>.&lt;/p>
&lt;p>But performance isn&amp;rsquo;t just about milliseconds. McKinsey Global Institute research reveals the business impact: retailers using big data to the full have potential to &lt;strong>increase operating margins by more than 60%&lt;/strong>. Organisations leveraging data-driven approaches are &lt;strong>23 times more likely to acquire customers, 6 times more likely to retain customers, and 19 times more likely to be profitable&lt;/strong>.&lt;/p>
&lt;p>These aren&amp;rsquo;t small companies or theoretical studies. McKinsey examined five domains globally including healthcare, retail, manufacturing, and public sector organisations.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-hidden-tax-of-bad-data">The Hidden Tax of Bad Data&lt;/h2>
&lt;hr>
&lt;p>Here&amp;rsquo;s what nobody tells you about skipping data modelling: &lt;strong>you pay for it anyway, just later and more expensively.&lt;/strong>&lt;/p>
&lt;p>Gartner research shows organisations lose an average of &lt;strong>$12.9 million annually&lt;/strong> due to poor data quality. IBM estimates US businesses lose &lt;strong>$3.1 trillion collectively&lt;/strong>. The Wakefield Research survey found over 50% of respondents indicated 25% or more of their revenue was affected by data quality issues.&lt;/p>
&lt;p>The &lt;strong>1x10x100 Rule&lt;/strong> quantifies escalating remediation costs: fixing a data quality issue at point of entry costs 1x, fixing after it propagates within systems costs 10x, and fixing after it reaches end-users and decision-making costs 100x.&lt;/p>
&lt;p>Let me give you some real-world disasters:&lt;/p>
&lt;p>&lt;strong>Unity Software&lt;/strong> lost &lt;strong>$110 million in revenue and $4.2 billion in market cap&lt;/strong> in 2022 from ingesting bad data from a large customer, causing 37% drop in share price and loss of investor confidence.&lt;/p>
&lt;p>&lt;strong>Samsung Securities&lt;/strong> suffered a &lt;strong>$300 million &amp;ldquo;fat-finger&amp;rdquo; error&lt;/strong> when an employee mistakenly paid 1,000 shares instead of 1,000 won per share in dividends, issuing $105 billion worth of shares before correction.&lt;/p>
&lt;p>&lt;strong>Equifax&lt;/strong> sent lenders inaccurate credit scores on millions of customers due to coding issues, dropping stock 5% and triggering class-action lawsuits.&lt;/p>
&lt;p>&lt;strong>JPMorgan Chase&amp;rsquo;s &amp;ldquo;London Whale&amp;rdquo;&lt;/strong> incident lost &lt;strong>$6.2 billion&lt;/strong>, exacerbated by errors in risk models relying on flawed data.&lt;/p>
&lt;p>&lt;strong>NASA&amp;rsquo;s Mars Climate Orbiter&lt;/strong> burned &lt;strong>$125 million&lt;/strong> due to unit mismatches between metric and imperial measurements.&lt;/p>
&lt;p>These aren&amp;rsquo;t abstract warnings. They&amp;rsquo;re real money disappearing from real balance sheets.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-productivity-drain-nobody-measures">The Productivity Drain Nobody Measures&lt;/h2>
&lt;hr>
&lt;p>Want to know where your data team&amp;rsquo;s time actually goes?&lt;/p>
&lt;p>Data scientists and analysts spend &lt;strong>60% of their time cleaning and organising data&lt;/strong> according to Forbes analysis, with 19% more spent searching for information. Anaconda&amp;rsquo;s State of Data Science surveys consistently show &lt;strong>39-45% of time on data preparation tasks&lt;/strong> rather than actual analysis.&lt;/p>
&lt;p>An IDC report found &lt;strong>44% of data workers&amp;rsquo; time is wasted on unsuccessful activities&lt;/strong>, with 33% (12.5 hours weekly) spent preparing information and 15% searching for data. For a team of 10 analysts, this represents &lt;strong>8,900+ hours of lost productivity annually&lt;/strong>.&lt;/p>
&lt;p>Think about that. Your highly skilled analysts—the people you hired to generate insights—are spending half their time just trying to figure out what the data means and how to structure it for analysis.&lt;/p>
&lt;p>Dimensional modelling solves this because the structure itself communicates meaning. An analyst looks at a star schema and immediately understands: here are the measurements, here&amp;rsquo;s the context, here&amp;rsquo;s how they relate. The modelling work gets done once by experts, then leveraged by everyone. It follows that DRY mantra of programming for years - Don&amp;rsquo;t Repeat Yourself.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-inconsistent-metrics-problem">The Inconsistent Metrics Problem&lt;/h2>
&lt;hr>
&lt;p>This is the silent killer of data team credibility.&lt;/p>
&lt;p>When three different tables represent the same business entity with different SQL logic, two analysts answering identical questions produce different results. Stakeholders lose confidence. Executive meetings devolve into debates where one department reports growing customer base while another reports increased churn.&lt;/p>
&lt;p>Simple metrics like Daily Active Users or Revenue get calculated differently by different teams. One defines DAU as logins, another as specific actions, a third as time spent—each producing wildly different results.&lt;/p>
&lt;p>Years back I was in a meeting where 2 GM&amp;rsquo;s literally argue about whether revenue is up or down because they&amp;rsquo;re looking at reports built on different data models (different teams data) with different business logic. The data team ends up spending hours reconciling numbers instead of generating insights.&lt;/p>
&lt;p>&lt;strong>Conformed dimensions and standardised fact tables eliminate this problem.&lt;/strong> There&amp;rsquo;s one product dimension. One customer dimension. One date dimension. One revenue fact. When everyone queries the same well-modelled tables, everyone gets the same answers.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="real-companies-real-scale-real-results">Real Companies, Real Scale, Real Results&lt;/h2>
&lt;hr>
&lt;p>Let&amp;rsquo;s look at organisations actually doing this at scale.&lt;/p>
&lt;h3 id="ubers-global-data-warehouse">Uber&amp;rsquo;s Global Data Warehouse&lt;/h3>
&lt;p>&lt;strong>Uber&amp;rsquo;s data lake consists of &amp;ldquo;foundational fact, dimension, and aggregate tables developed using dimensional data modelling techniques&amp;rdquo;&lt;/strong> accessible to engineers and data scientists in a self-serve manner.&lt;/p>
&lt;p>The scale is staggering:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>100+ petabytes&lt;/strong> of data in HDFS&lt;/li>
&lt;li>&lt;strong>100,000 vcores&lt;/strong> in compute cluster&lt;/li>
&lt;li>&lt;strong>100,000 Presto queries per day&lt;/strong>&lt;/li>
&lt;li>&lt;strong>10,000 Spark jobs per day&lt;/strong>&lt;/li>
&lt;li>&lt;strong>20,000 Hive queries per day&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>Their incremental ETL approach using Apache Hudi delivered &lt;strong>50% decrease in pipeline run time and 60% decrease in SLA&lt;/strong>, supporting minute-level latency for petabyte-scale data.&lt;/p>
&lt;p>As Uber&amp;rsquo;s engineering blog states directly: &amp;ldquo;The data lake consists of foundational fact, dimension, and aggregate tables developed using dimensional data modelling techniques that can be accessed by engineers and data scientists in a self-serve manner to power data engineering, data science, machine learning, and reporting across Uber.&amp;rdquo;&lt;/p>
&lt;h3 id="netflixs-unified-data-architecture">Netflix&amp;rsquo;s Unified Data Architecture&lt;/h3>
&lt;p>Netflix integrates dimensional models for operational reporting while managing data from &lt;strong>500+ microservices globally&lt;/strong>. Their data warehouse built on Apache Iceberg tables uses GraphQL integration with knowledge-graph-based design for consistent data modelling.&lt;/p>
&lt;p>This powers Sphere, Netflix&amp;rsquo;s self-service operational reporting tool for business users, handling &lt;strong>multiple petabytes&lt;/strong> of data. The architecture demonstrates dimensional modelling works seamlessly with modern streaming and microservices architectures.&lt;/p>
&lt;h3 id="spotifys-event-processing">Spotify&amp;rsquo;s Event Processing&lt;/h3>
&lt;p>Spotify processes &lt;strong>500 billion events per day&lt;/strong> (70TB compressed daily) running &lt;strong>20,000 batch data pipelines&lt;/strong> across 1,000+ repositories. Their Google Cloud Platform implementation uses BigQuery for dimensional models supporting podcast analytics and business reporting.&lt;/p>
&lt;p>The company attributes &lt;strong>75% year-over-year ad revenue growth&lt;/strong> to data-driven insights enabled by their dimensional data architecture.&lt;/p>
&lt;h3 id="airbnb-facebook-and-lyfts-snapshot-approach">Airbnb, Facebook, and Lyft&amp;rsquo;s Snapshot Approach&lt;/h3>
&lt;p>These companies pioneered snapshot-based dimensional modelling, a technique developed by Maxime Beauchemin (senior Lyft data engineer) that modernizes slowly changing dimensions for cloud environments.&lt;/p>
&lt;p>Rather than complex SCD Type 2 logic, they create daily or weekly table partitions as snapshots of dimensional data, leveraging cheap cloud storage. Beauchemin&amp;rsquo;s philosophy: &lt;strong>&amp;ldquo;Compute is cheap. Storage is cheap. Engineering time is expensive.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>This pragmatic adaptation of Kimball principles for cloud-native environments exemplifies how dimensional modelling evolves without abandoning core principles.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-healthcare-and-finance-proof-points">The Healthcare and Finance Proof Points&lt;/h2>
&lt;hr>
&lt;p>Want proof dimensional modelling works in regulated environments with strict compliance requirements?&lt;/p>
&lt;p>&lt;strong>Piedmont Healthcare&lt;/strong> manages &lt;strong>15 TB of data&lt;/strong> from 16 hospitals and 1,400 physician practices serving &lt;strong>2+ million patient visits annually&lt;/strong>. Their migration to Exasol with dimensional models reduced dashboard refresh time from &lt;strong>10 minutes to seconds&lt;/strong> while delivering &lt;strong>20 new metrics per month&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Kaiser Permanente&amp;rsquo;s HealthConnect&lt;/strong> integrates millions of patient records using dimensional models, enabling reduced hospital stays through early risk identification and improved chronic disease management. These implementations prove dimensional modelling satisfies stringent HIPAA and regulatory requirements while delivering performance.&lt;/p>
&lt;p>&lt;strong>JPMorgan Chase&lt;/strong> uses sophisticated dimensional data warehouse infrastructure for risk management, regulatory compliance, and personalized banking experiences.&lt;/p>
&lt;p>&lt;strong>Target Corporation&amp;rsquo;s Guest Data Platform&lt;/strong> integrates dimensional models for unified customer views, powering highly successful personalized marketing campaigns and optimised store layouts.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-roi-study-that-ends-the-debate">The ROI Study That Ends the Debate&lt;/h2>
&lt;hr>
&lt;p>An International Data Corporation study of 62 organisations documented &lt;strong>average ROI of 401% over three years&lt;/strong>, excluding failed projects and extreme outliers.&lt;/p>
&lt;p>Common metrics show:&lt;/p>
&lt;ul>
&lt;li>Payback periods of &lt;strong>1-3 years&lt;/strong>&lt;/li>
&lt;li>Positive net present value in &lt;strong>75%+ of implementations&lt;/strong>&lt;/li>
&lt;li>&lt;strong>15-30% reduction in operational costs&lt;/strong>&lt;/li>
&lt;li>&lt;strong>10-25% increase in data-driven revenue opportunities&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>These aren&amp;rsquo;t theoretical benefits. They represent measured outcomes from actual enterprise implementations.&lt;/p>
&lt;p>Meanwhile, industry adoption rates paint a clear picture: &lt;strong>90%+ of enterprises globally&lt;/strong> utilise data warehousing infrastructure, with 58% of deployments cloud-based. The data warehousing market reached &lt;strong>$34.5-35 billion in 2024&lt;/strong>, projected to grow to &lt;strong>$75-93 billion by 2033&lt;/strong>.&lt;/p>
&lt;p>Joe Reis, prominent data engineering consultant, observes: &lt;strong>&amp;ldquo;Kimball is more in use than any other point in time due to being the default way to model data in Self-Service Business Intelligence applications (BI): Power BI and Tableau. And BI is 10 times bigger in usage than data engineering.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="modern-platforms-embrace-dimensional-modelling">Modern Platforms Embrace Dimensional Modelling&lt;/h2>
&lt;hr>
&lt;p>Here&amp;rsquo;s where the &amp;ldquo;dimensional modelling is dead&amp;rdquo; crowd gets it completely wrong. Modern cloud platforms haven&amp;rsquo;t replaced dimensional modelling—they&amp;rsquo;ve made it easier to implement.&lt;/p>
&lt;h3 id="snowflakes-explicit-recommendation">Snowflake&amp;rsquo;s Explicit Recommendation&lt;/h3>
&lt;p>Snowflake&amp;rsquo;s official blog states: &lt;strong>&amp;ldquo;When using Snowflake, store the raw data history in a structured or variant format, clean and fit the data using the third normal form or the Data Vault model, and it makes sense to store the final consumable data in the Kimball dimensional data model.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>The company notes each data model has advantages and &amp;ldquo;storing intermediate step results has significant architectural advantages.&amp;rdquo; Snowflake&amp;rsquo;s columnar storage, automatic query optimisation for star joins, and separation of compute and storage complement dimensional designs perfectly.&lt;/p>
&lt;h3 id="databricks-lakehouse-approach">Databricks&amp;rsquo; Lakehouse Approach&lt;/h3>
&lt;p>Databricks&amp;rsquo; official guidance emphasises: &lt;strong>&amp;ldquo;A large number of our customers are migrating their legacy data warehouses to Databricks Lakehouse&amp;hellip; As we help customers in the field, we find that many are looking for best practices around proper data modelling and physical data model implementations in Databricks. For Data Warehousing, Analytics-friendly modelling styles like Star-schema and Data Vault are quite popular.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>The lakehouse medallion architecture (Bronze/Silver/Gold layers) typically implements dimensional models in the Gold layer for business consumption. Databricks supports identity columns for surrogate keys, primary/foreign key constraints, Z-order clustering for performance, and column-level data quality constraints—all features that facilitate dimensional modelling.&lt;/p>
&lt;h3 id="bigquerys-optimisation">BigQuery&amp;rsquo;s Optimisation&lt;/h3>
&lt;p>BigQuery&amp;rsquo;s architecture optimises for dimensional models. The platform&amp;rsquo;s columnar storage, partitioning, clustering, and primary/foreign key hints enable query optimisers to efficiently execute dimensional model queries. Type 2 dimension tables at the same grain as fact tables can be flattened into physical tables, while the logical model maintains clear dimensional structure.&lt;/p>
&lt;h3 id="microsoft-fabrics-position">Microsoft Fabric&amp;rsquo;s Position&lt;/h3>
&lt;p>Microsoft Fabric&amp;rsquo;s official documentation states unequivocally: &lt;strong>&amp;ldquo;A star schema design is optimised for analytic query workloads. For this reason, it&amp;rsquo;s considered a prerequisite for enterprise Power BI semantic models.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>The documentation emphasises that while specific circumstances might suggest alternatives, &amp;ldquo;the theory of dimensional modelling is still relevant. That theory helps analysts create intuitive and efficient models.&amp;rdquo;&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-fortune-500-case-study-nobody-talks-about">The Fortune 500 Case Study Nobody Talks About&lt;/h2>
&lt;hr>
&lt;p>Dustin Dorsey (Principal Data Architect at Onyx) presented at the Open Data Science Conference in April 2024, busting modern myths about dimensional modelling.&lt;/p>
&lt;p>He demonstrated a Fortune 500 company using dbt and Databricks that was &lt;strong>&amp;ldquo;drowning in complexity&amp;rdquo;&lt;/strong> with one massive dbt model containing over 1,000 lines of code.&lt;/p>
&lt;p>After implementing dimensional modelling:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>21 modular dbt models&lt;/strong> replaced the monolith&lt;/li>
&lt;li>&lt;strong>179 tests&lt;/strong> replaced 2&lt;/li>
&lt;li>&lt;strong>6 measurable columns&lt;/strong> replaced 1&lt;/li>
&lt;li>&lt;strong>250+ accessible attributes&lt;/strong> became available&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&amp;ldquo;Analysts went from reading 1,000 lines of code to writing 20.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>Performance remained comparable but with significantly better governance, scalability, and business usability.&lt;/p>
&lt;p>This is the pattern I see repeatedly: teams start with the promise of agility and flexibility, build increasingly complex models to handle reality, then eventually refactor back to proper dimensional modelling after the pain becomes unbearable.&lt;/p>
&lt;p>Why not start with the proven approach?&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-technical-debt-time-bomb">The Technical Debt Time Bomb&lt;/h2>
&lt;hr>
&lt;p>McKinsey research shows organisations spend &lt;strong>20-40% of technology estate value managing technical debt&lt;/strong>. Stripe&amp;rsquo;s engineering study found teams spend an average of &lt;strong>33% of their time managing technical debt&lt;/strong>—time that could drive innovation and growth.&lt;/p>
&lt;p>Database migration horror stories illustrate accumulated debt coming due. Datometry analysis shows &lt;strong>300-500% cost overruns&lt;/strong> are more common than expected in data warehouse migrations, with consulting fees for large migrations reaching tens of millions, sometimes &lt;strong>exceeding $100 million for Fortune 100 projects&lt;/strong>.&lt;/p>
&lt;p>Hidden costs include:&lt;/p>
&lt;ul>
&lt;li>Upgrading all ETL, BI, and reporting systems simultaneously&lt;/li>
&lt;li>Business disruptions lasting weeks or months&lt;/li>
&lt;li>Compliance re-certification for regulated workloads&lt;/li>
&lt;li>Schema mismatch analysis and transformation&lt;/li>
&lt;li>Data cleansing for decades of dirty data&lt;/li>
&lt;/ul>
&lt;p>One mid-sized e-commerce company with approximately 50 million users attempted MySQL to MongoDB migration with fundamentally broken data modelling—trying to recreate relational tables in a document database.&lt;/p>
&lt;p>Product catalog queries requiring 15+ joins saw response times balloon from &lt;strong>50 milliseconds to 8 seconds&lt;/strong>. A Black Friday migration created data inconsistencies. AWS costs exploded from expected &lt;strong>$15,000 monthly to $47,000 the first month&lt;/strong>.&lt;/p>
&lt;p>This is what happens when you skip proper modelling and accumulate technical debt.&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="the-hybrid-approach-using-the-right-tool">The Hybrid Approach: Using the Right Tool&lt;/h2>
&lt;hr>
&lt;p>Modern consensus has emerged around a &lt;strong>polyglot data modelling strategy&lt;/strong>: use multiple modelling techniques appropriately.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Data Vault or normalised models in integration layers (Silver)&lt;/strong> provide auditability and flexibility for source system changes&lt;/li>
&lt;li>&lt;strong>Dimensional models in presentation layers (Gold)&lt;/strong> provide query performance and business user accessibility&lt;/li>
&lt;li>&lt;strong>Raw data in Bronze layers&lt;/strong> enables exploratory analysis&lt;/li>
&lt;/ul>
&lt;p>This approach uses the right model for the right use case rather than forcing a single methodology everywhere.&lt;/p>
&lt;p>As Margy Ross, co-author with Ralph Kimball and president of DecisionWorks Consulting with 35+ years teaching dimensional modelling to over 10,000 students, addressed directly in 2017:&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;The short answer is &amp;lsquo;yes.&amp;rsquo; The need to focus on business process measurement events, plus grain, dimensions and facts, is as important as ever. Dimensional modelling has helped countless organisations across every industry make better business decisions which should be the true measure of DW/BI success. While it&amp;rsquo;s fun to try something new, why dismiss a proven, valuable technique that has provided positive return on investment?&amp;rdquo;&lt;/strong>&lt;/p>
&lt;/br>
&lt;/br>
&lt;h2 id="what-actually-works-in-2025">What Actually Works in 2025&lt;/h2>
&lt;hr>
&lt;p>The organisations thriving in modern data environments aren&amp;rsquo;t those that abandoned modelling for raw data lakes. They&amp;rsquo;re the ones that adapted dimensional modelling principles to new platforms, combining proven analytical design patterns with modern engineering practices.&lt;/p>
&lt;p>Uber&amp;rsquo;s 100+ petabyte dimensional data lake, Netflix&amp;rsquo;s unified data architecture, and Spotify&amp;rsquo;s 500 billion daily events all demonstrate dimensional modelling scales to modern big data volumes.&lt;/p>
&lt;p>For teams building modern data platforms, the question isn&amp;rsquo;t whether to use dimensional modelling—it&amp;rsquo;s how to implement Kimball principles with tools like dbt, cloud warehouses like Snowflake and Databricks, and lakehouse architectures.&lt;/p>
&lt;p>&lt;strong>Start with business process identification and careful grain declaration.&lt;/strong> Build conformed dimensions that enable integration across subject areas. Implement fact tables that capture measurements at atomic grain. Use modern tools like dbt for modular, testable transformations. Leverage cloud platform features for performance and scale. Test rigorously and document thoroughly.&lt;/p>
&lt;p>The methodology Ralph Kimball introduced nearly three decades ago continues delivering value because it addresses fundamental challenges in organising data for analytics—challenges that persist regardless of underlying technology.&lt;/p>
&lt;p>The evidence is overwhelming. The ROI is measurable. The alternatives are expensive.&lt;/p>
&lt;p>Dimensional modelling isn&amp;rsquo;t dead. It&amp;rsquo;s just getting started.&lt;/p>
&lt;/br>
&lt;/br>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;hr>
&lt;ol>
&lt;li>Analytics8. &amp;ldquo;Data Modelling in the Modern Data Stack.&amp;rdquo; &lt;em>Analytics8 Blog&lt;/em>. &lt;a href="https://www.analytics8.com/blog/data-modeling-in-the-modern-data-stack/" target="_blank" rel="noopener">https://www.analytics8.com/blog/data-modeling-in-the-modern-data-stack/&lt;/a>&lt;/li>
&lt;li>Gartner Research. &amp;ldquo;The State of Data Quality 2024.&amp;rdquo; &lt;em>Gartner&lt;/em>.&lt;/li>
&lt;li>DBSync. &amp;ldquo;The Cost of Poor Data Quality.&amp;rdquo; &lt;em>DBSync Research&lt;/em>.&lt;/li>
&lt;li>McKinsey &amp;amp; Company. &amp;ldquo;Big Data: The Next Frontier for Innovation, Competition, and Productivity.&amp;rdquo; &lt;em>McKinsey Global Institute&lt;/em>.&lt;/li>
&lt;li>Uber Engineering. &amp;ldquo;Building Uber&amp;rsquo;s Data Lake.&amp;rdquo; &lt;em>Uber Engineering Blog&lt;/em>.&lt;/li>
&lt;li>GeeksforGeeks. &amp;ldquo;Dimensional Modelling in Data Warehouse.&amp;rdquo; &lt;em>GeeksforGeeks&lt;/em>.&lt;/li>
&lt;li>Data-Sleek. &amp;ldquo;The Four-Step Dimensional Design Process.&amp;rdquo; &lt;em>Data-Sleek Blog&lt;/em>.&lt;/li>
&lt;li>McKinsey &amp;amp; Company. &amp;ldquo;Analytics Comes of Age.&amp;rdquo; &lt;em>McKinsey Research&lt;/em>.&lt;/li>
&lt;/ol></content:encoded><category>Data Engineering</category><category>Data Modelling</category><category>Dimensional Modelling</category><category>Data Warehouse</category><category>Data Modelling</category><category>Data Quality</category><category>Analytics</category><category>Kimball</category><category>Big Data</category></item></channel></rss>