Note 10 · Microsoft Fabric (4/11)

Semantic Migration: How Do You Rebind Reports When the Model Moves to Lakehouse?

Why lakehouse migrations fail on NULL versus zero, float drift, and type rebase settings, and how to treat parity as a contract.

12 August 2026 9 min read

Key takeaways

  • Treat migration as written semantic equivalence, not a file move into Delta.
  • Write NULL, empty, and zero rules by column class before cutover.
  • Automate compares on key tables with named tolerances; row counts alone lie.
  • Pin Spark rebase, timezone, and type policy in jobs that write history.
  • Type as early as practical in silver; gold should not invent the first decimal.
  • Include a measure and relationship pack in definition of done, with reviewer sign-off.

Copying tables into a lakehouse is the easy part. The hard part is whether 0 means zero or was null in the legacy system, and whether a 0.01 drift is rounding or a bug.

I treat migration as a semantic contract, not a file move. Nobody celebrates that Delta exists. People care that a known headline number still matches within agreed rules.

Named model: parity as a contract

  • Legacy semantics: Import or DirectQuery meaning for NULL, 0, empty, blanks, numeric values, dates, flags, measures, and relationships.
  • Written contract: column-class rules, named tolerances, pinned Spark rebase and timezone, early typing in silver, and a frozen measure pack.
  • Lakehouse semantics: Delta and Direct Lake outputs that must prove equivalence under those rules, not under a vibe that "the numbers look close."
  • Contract: bytes landing is not acceptance. Meaning survives the engine change only when automated compares and report sign-off say so.

Named model: legacy semantics, written contract, and lakehouse semantics must stay equivalent under rules you can show in a review

Figure 1. Parity as a contract. Legacy meaning and lakehouse meaning stay equivalent under written rules. Busy Delta folders are not the celebration.

Context and constraint

Cutovers from Import and DirectQuery onto Fabric lakehouses can show perfect row counts and still get rejected in review. The bytes landed. The meaning did not. That gap is the real migration work.

Moving from classic Power BI Import or DirectQuery into a lakehouse-backed model changes engines, null handling, type coercion, and often the grain of intermediate tables. If you only validate that tables exist and schemas look familiar, you will discover measure drift in a review meeting. Treat NULL versus 0, numeric precision, dates, flags, and measures as first-class contract clauses when reports are the acceptance surface.

Figure 2 is the Learn surface for schemas and shortcuts. Location changes where data appears. It does not prove what the values mean.

Lakehouse schemas and shortcuts in Microsoft Fabric

Figure 2. Shortcuts and schemas change where data appears, not what it means. Source: Microsoft Learn: lakehouse schemas.

Design

Parity bugs that look fine in a demo

NULL versus 0. Legacy empty-string nulling patterns become integers that keep 0. Counts, averages, and joins shift. A filter that once excluded unknowns now includes zeros as real values. The reverse also happens: missing stored as 0 becomes NULL in silver, and "active" counts drop.

Float drift. Values that differ by 0.01 across rows explode into sum gaps you will debate for weeks. Import sometimes hid this via load or model rounding. Spark and Delta keep the float noise unless you decide decimal types and rounding early.

Datetime rebase modes. Spark legacy versus correct rebase settings change history silently if you do not pin them. A timestamp that looked fine in a sample month breaks around historical DST edges. Cluster-default notebooks are not reproducible migration tools.

Type once, early. Casting in gold while bronze stays stringy means every consumer re-learns the same bugs. DirectQuery often deferred typing to the engine; delayed lakehouse typing recreates that ambiguity with more hops.

Measure drift. Even when fact columns match, DAX that assumed Import cardinality, blank handling, or relationship behavior can disagree with Direct Lake or a re-modeled star. Parity is ETL plus the semantic model contract.

Figure 3 is the column-class map that stops every table inventing a private dialect.

Equivalence rules by column class for NULL versus zero, numeric values, dates, flags, early typing, and named tolerances

Figure 3. Equivalence rules by column class. Argue sentinel zero and float policy before go-live. Widening epsilon after is industrializing the bug.

1. Define equivalence rules per column class

Write rules for numeric values, quantities, flags, empty strings, dates, and identifiers. Example classes: "empty string in source becomes NULL in silver," "legacy 0 on a nullable numeric column means unknown, not zero," "a decimal value rounds half-up to 4 decimals before gold." Without classes, every table invents a private dialect.

2. Automate row and aggregate compares for key tables

Compare keys, null rates, distinct counts, and numeric sums between legacy extracts and lakehouse outputs. Row counts alone lie. Keep tolerances explicit: exact for keys and flags, bounded epsilon only where float is unavoidable and documented.

3. Pin Spark session settings in history-sensitive notebooks

Rebase modes, timezones, and parser options belong in the notebook or job definition, not in whoever last touched the cluster. Migration history must be reproducible. Silent default changes are defects.

4. Type as early as practical in the lineage

Promote decimals, dates, and booleans in silver when the rule is known. Gold should not be the first place a decimal exists if five notebooks already summed the string column differently. Early typing also makes Direct Lake and SQL endpoint consumers less surprising.

5. Include measures and relationships in the parity pack

For report cutovers, freeze a small suite of page-level and measure-level compares against a known legacy workspace. Document relationship differences (single versus both, missing members, blank row behavior). If Import used a calculated column that lakehouse pushes upstream, name that rewrite so nobody "fixes" it back in DAX after go-live.

Figure 4 is why pipeline green is not cutover. ETL compares and the measure pack are both required.

Parity pack with ETL compares on key tables and a frozen measure and relationship pack for report cutover

Figure 4. Definition of done includes ETL parity jobs and a measure pack signed by reviewers. Declaring ETL done first is how review meetings find the drift.

Figure 5 is the Learn framing for moving content across stages. Stage promotion does not prove semantic parity.

Deployment pipeline paired items across stages

Figure 5. Moving content across stages does not prove semantic parity. Source: Microsoft Learn: deployment pipelines.

Common mistakes

Row counts match while numeric columns drift. The demo passes. Sign-off fails you.

Fixing tolerance in validation instead of fixing the write path. Widening epsilon until green is how you industrialize the bug.

One giant notebook cell that hides which table failed parity. Failures must name table, column class, and rule. Otherwise debugging becomes archaeology.

Declaring ETL done before measure packs are compared. People experience migration through reports. Meet them there.

Assuming DirectQuery blank behavior equals lakehouse NULL behavior. Engines disagree until you force the contract.

Shortcuts and schema moves treated as meaning-preserving. Location changed. Semantics still need proof.

Use as-is or adapt

Use as-is

  • Written NULL, empty, and zero rules by column class.
  • Parity jobs for key tables with named tolerances.
  • Explicit Spark rebase, timezone, and type policy in jobs that write history.
  • Where types are enforced in the medallion (bronze stay raw versus typed silver).
  • Measure and relationship compare pack for report cutover.
  • Definition of done that includes reviewer sign-off on parity reports, not only pipeline green.

Adapt before you use

Real parity work slows the first cutover. You will argue whether legacy 0 was a real zero or a missing sentinel. That argument is cheaper before go-live than after. Strict typing early can break dirty bronze that used to "just load" into Import; that pain usually surfaces data quality while you still have rollback. Automated compares need stable legacy extracts and keys. Without reproducible snapshots, parity jobs go flaky and people stop trusting them.

Primary references: Lakehouse schemas, Lakehouse overview, and Microsoft Learn guidance on Direct Lake and semantic models for Power BI cutovers.

Land the files. Prove NULL, zero, numeric values, dates, and measures under rules you can show in a review. Lakehouse migration succeeds when meaning survives the engine change, not when Delta folders look busy.