The Accountability Gap
A Microsoft Fabric warehouse exercise in defensive T-SQL, a five-level hierarchy chain, and an SCD Type 2 MERGE that closes a record instead of overwriting it.
Veritas Global is a forty-two-billion-dollar asset manager reporting to three regulators, each of which can ask who was accountable for a flagged position on a specific date in the past. Its Relationship Manager roster was a flat table that got overwritten every time someone changed desk or title, so the honest answer to that question was often “we no longer know.” The brief was a Microsoft Fabric warehouse layer that stops trusting dirty source data blindly, resolves a five-level reporting chain in a single query, and never erases a personnel record again.
The problem
Three regulators (the FCA, the SEC, and MAS) each require point-in-time attribution: not who manages an account today, but who held it when a flagged position was opened. Veritas Global’s roster table couldn’t answer that, because every promotion, desk move, or jurisdiction transfer updated the existing row in place. The firm’s Global Head of Risk put it plainly after an FCA review asked for the record on a specific date: “We could not answer it. Not because the data was complex. Because we had overwritten the record when the team restructured in November.”
Three compounding failures sat behind that gap. Source telemetry arrived dirty, notional values as text with embedded symbols, dates in inconsistent formats, and a direct CAST on any of it could fail an entire batch silently. The reporting line ran five levels deep, from Analyst to Managing Partner, deeper than a standard join could resolve. And the roster itself preserved zero historical states: every update destroyed the version before it.
The approach
Three structural changes close those three failures. A governed gold view absorbs dirty source data with defensive casting, so a bad row gets classified rather than dropped. A self-join query walks the five-level hierarchy in one pass instead of failing on it. And the flat roster becomes an SCD Type 2 table, where a personnel change closes the old record and opens a new one rather than overwriting anything. One honest limit sits underneath all three: the structure protects every roster change from the day it goes live, but it cannot reconstruct a state that was already overwritten before that.
Land the source tables
Three schemas separate the layers: ref for the roster, stg for raw positions, gold for the governed output. A Data Factory Copy Data activity then pulls both source tables from the upstream Azure SQL database into the Fabric warehouse, one pipeline task per table.
View full size (opens in a new tab)
View full size (opens in a new tab)
View full size (opens in a new tab)
View full size (opens in a new tab)
View full size (opens in a new tab)The governed gold view
gold.clean_positions is where dirty data gets absorbed rather than allowed to fail a pipeline. Every numeric and date column from staging goes through TRY_CAST, which returns NULL on a bad value instead of raising an error, and a missing RM is resolved with ISNULL(r.Name, 'Unassigned') rather than dropped by the join.
View full size (opens in a new tab)
View full size (opens in a new tab)
View full size (opens in a new tab)Look closely at the Exposure_Category logic and one row exposes a real gap in it. The Medium_Exposure branch reads >= 10000000 AND < 9999999, a range that can never be true since ten million is already past nine million, nine hundred and ninety-nine thousand. POS-0008, with a notional of 52,339,391, falls through every branch and lands on Out_of_Range in the first results screenshot above, when it should sit in a mid-tier band. The defensive casting works exactly as designed. The threshold above it has a typo that quietly reclassifies most of the mid-range portfolio.
The RM performance leaderboard
Ranking Relationship Managers by resolved, regulator-signed-off positions, partitioned so RMs only compete against peers, uses DENSE_RANK rather than RANK: two RMs tied on the same count get the same rank, and the RM below them gets the next rank down rather than one skipped to make room.
View full size (opens in a new tab)
View full size (opens in a new tab)Flatten the five-level hierarchy
ref.Relationship_Managers self-joins to itself four times, once per step up the ManagerID chain from Analyst to Managing Partner, so the complete reporting line resolves in a single query rather than a recursive loop.
View full size (opens in a new tab)
View full size (opens in a new tab)Hierarchy_Depth here counts how many people are in the chain from that RM up to the top, five for the Analyst at the bottom, one for the Managing Partner who has no manager above them. It’s a different number from the “regulatory significance level” in the brief, which ran the other way, one for Analyst and five for Managing Partner. Both describe the same five-step ladder; they just count from opposite ends.
Make roster history survive change
This is the part the whole case study is named for. The roster gets three new columns, start_date, end_date, and is_active, and every existing row is backfilled to an active state before any real change ever reaches it.
View full size (opens in a new tab)
View full size (opens in a new tab)Incoming changes land in stg.New_RM_Roster, a feed of only the RMs who were promoted, moved, or newly hired, not a full resync of the roster. A staging query tags each incoming row twice: once with its real RMID as MergedKey, but only if that RM already exists as an active record, and once with MergedKey forced to NULL, unconditionally.
View full size (opens in a new tab)
View full size (opens in a new tab)Make one change produce two rows, not zero or four
The MERGE has to close the old version of a changed RM's record and open a new one, in a single atomic statement, without double-processing an unchanged RM and without letting a changed RM's own two staging rows both fire the same outcome.
The MergedKey trick from the staging query does the routing. The ON clause is S.MergedKey = T.RMID AND T.is_active = 1. The matched-copy row (real MergedKey) finds the existing active record and enters WHEN MATCHED, where an AND block checks whether name, title, manager, jurisdiction, or desk actually differ, if none of them do, no action fires for that pairing at all. The NULL-copy row can never satisfy S.MergedKey = T.RMID, because SQL never treats NULL as equal to anything, so it always falls to WHEN NOT MATCHED THEN INSERT.
For a genuinely changed RM, that produces exactly the pair the acceptance criteria asked for: the matched copy closes the old row (end_date set, is_active = 0), and the NULL copy inserts the new one. The two outcomes can’t collide, because they’re never the same row hitting the same clause, they’re two different staging rows built for two different jobs.
It’s worth naming the edge this doesn’t fully cover: the insert side has no equivalent “only if something actually changed” guard. If stg.New_RM_Roster ever carried a resubmission of an unchanged record, its NULL-copy row would still insert a new active version, because WHEN NOT MATCHED alone doesn’t check whether anything is different. The design leans on the upstream feed only ever containing genuine changes, which held for this run, but isn’t enforced by the MERGE itself.
The MERGE statement went through two passes. The first wired up the routing logic with a placeholder where the staging query would go. The second substituted the real subquery in and labelled which side was which.
View full size (opens in a new tab)
View full size (opens in a new tab)
View full size (opens in a new tab)What this proves
A view can absorb source-data failures without a pipeline going down, a five-level hierarchy can resolve in one query instead of a recursive loop, and a personnel change can become history instead of erasing it, all inside plain T-SQL. But the honest answer to the question this case study is named for is still no. The SCD Type 2 structure protects every roster change from the day it goes live forward. It cannot reconstruct the state of a record that was already overwritten before that date. Telling a regulator what a system can and cannot prove, in the same breath as showing them the fix, is part of the engineering, not an afterthought to it.