Lakehouse Digest
ISSUE 019 · OCTOBER 7, 2026

Your Delta stream dies the day someone runs an UPDATE — change data feed is the fix

Structured Streaming accepts append inputs only — one UPDATE, DELETE, MERGE, or OVERWRITE on the source table fails the stream. Change data feed is the documented fix when corrections must reach downstream, but it records changes only from the moment you enable it.

THE THREE-PART READ

Appends are a contract, not a habit.

Structured Streaming trusts that a Delta source only ever appends. The day an UPDATE, DELETE, MERGE, or OVERWRITE lands, that trust breaks and the stream fails. This issue: the contract, the docs' four approaches, and the change-data-feed cutover that keeps corrections flowing.

TIP

One UPDATE kills the stream

Streaming reads accept append inputs only; any modifying commit fails the stream.

See the contract →
IMPLEMENTATION

Four approaches, one robust path

skipChangeCommits, full refresh, change data feed, materialized views — and the docs' recommendation.

Choose and cut over →
DEV Q&A

Restatements kill my silver stream

skipChangeCommits if corrections don't matter; a CDF read if they must propagate.

Read the answer →

Last verified against official Databricks documentation: October 7, 2026. Correction history: first publication — no corrections.

One modifying commit breaks a plain stream

Structured Streaming treats a Delta source as an append-only log. The official contract is blunt: any UPDATE, DELETE, MERGE INTO, or OVERWRITE on the source table fails the stream with an error. There is no “tolerate small changes” mode — the day a monthly restatement or a GDPR delete lands on bronze, the silver stream reading it dies on the next micro-batch.

This is why append-only tables stream for months and then break overnight: the failure is not in your query, it is in the source's write pattern changing. The fix is a decision, made before the restatement lands.

-- 1. Enable legacy CDF on the source (Delta tables only).
ALTER TABLE bronze.orders
  SET TBLPROPERTIES ('delta.enableChangeDataFeed' = 'true');

-- 2. Or default it on for all NEW tables in this session:
SET spark.databricks.delta.properties.defaults.enableChangeDataFeed = true;

-- 3. Verify before you cut over — must return true:
SHOW TBLPROPERTIES bronze.orders ('delta.enableChangeDataFeed');
The painful middle is the trap: skipChangeCommits=true drops every correction — they never reach downstream. If the corrections matter, that is data loss with extra steps. Read the feed instead.

The four documented approaches

When the source table can change, the official documentation compares four approaches. skipChangeCommits is the simple appends-only escape hatch — it ignores modifying commits entirely, and corrections never propagate; the docs recommend it “for most workloads that do not use change data feeds.” Full refresh is simple but expensive at scale. Change data feed processes every change type and is “the most robust approach because your code explicitly handles every type of change event.” Materialized views propagate changes automatically but at higher latency. The standing recommendation: “streaming from the CDC feed of a Delta Lake table rather than directly from the table whenever possible.”

Step 1 — BEFORE: enable the feed (it never backfills)

Illustrative, idempotent setup:

CREATE OR REPLACE TABLE demo.cdf_demo (
  order_id BIGINT, amount DOUBLE
) TBLPROPERTIES ('delta.enableChangeDataFeed' = 'true');  -- version 0
INSERT INTO demo.cdf_demo VALUES (1, 100.00), (2, 200.00), (3, 300.00);  -- version 1
UPDATE demo.cdf_demo SET amount = 150.00 WHERE order_id = 1;             -- version 2

Legacy change data feed records only changes that occur after it is enabled. Enable it on day one — a restatement that lands before enablement is invisible to the feed forever, and an off-then-on gap stays unqueryable.

Step 2 — READ: every change, typed

SELECT _change_type, order_id, amount, _commit_version
FROM table_changes('demo.cdf_demo', 0)
ORDER BY _commit_version, order_id;

Illustrative expected output — expected shape, not measured: version 1 contributes three insert rows; version 2 contributes one update_preimage row (order_id 1, amount 100.00 — the value before) and one update_postimage row (order_id 1, amount 150.00 — the value after). Each record also carries _commit_version and _commit_timestamp; delete is the fourth change type. A table whose schema already uses those column names cannot enable CDF until they are renamed.

The streaming trap — illustrative, mirroring an October 2026 community incident: a stream reading demo.cdf_demo with plain append semantics runs cleanly through version 1; the moment the UPDATE lands at version 2, the next micro-batch fails. The docs' contract is “throws an exception if any modifications occur on the source Delta Lake table.” Exact error text varies by runtime; the docs guarantee the failure, not a specific message.

Step 3 — CUTOVER: skip the snapshot replay

Illustrative recipe; verified option names only:

(spark.readStream
  .format("delta")
  .option("readChangeFeed", "true")
  .option("startingVersion", 76)      # last-processed version + 1
  .table("bronze.orders")
  .writeStream
  .format("delta")
  .option("checkpointLocation", "/mnt/checkpoints/silver_orders_cdf_v2")  # NEW location
  .start("silver.orders"))

When a CDF stream first starts without a starting bound, it returns the latest snapshot as INSERT records. If the target is already caught up, cut over with startingVersion = last-processed + 1 against a new checkpoint location so that the new starting bound takes effect. An existing checkpoint resumes its saved state instead. A version older than CDF enablement raises an error; a version aged out of table history fails the stream start.

Step 4 — GATES: make the regression fail loudly

-- Gate 1: CDF must be ON before the cutover. Must return true.
SHOW TBLPROPERTIES demo.cdf_demo ('delta.enableChangeDataFeed');
-- Gate 2: never read below the enable version.
DESCRIBE HISTORY demo.cdf_demo;  -- find where the property landed

Gate 1 is a real gate — the cutover must not proceed unless the property is on. Gate 2 leans on the engine: the docs' contract is that a table_changes start version older than CDF enablement raises an error, so start >= enable version is enforced whether the pipeline checks it or not.

Automatic versus legacy CDF

Automatic CDF requires DBR 19+ and a Unity Catalog-registered table; Delta tables need row tracking enabled. It is computed at query time, needs no per-table CDF configuration, and the docs recommend migrating to it. Legacy CDF is materialized at write time, works only with Delta tables, and requires the per-table property. You cannot use both together.

CDF is transient: a change data feed is “not intended to serve as a permanent record of all changes to a table.” Old versions and their CDF age out with table retention; VACUUM deletes change data files. For permanent history, incrementally write CDF output into a separate table.
  1. Confirm enablement: SHOW TBLPROPERTIES <table> ('delta.enableChangeDataFeed') must return true before any cutover.
  2. Find the enable version: DESCRIBE HISTORY — never set a table_changes start or readChangeFeed startingVersion below it; the read raises an error.
  3. Decide: skipChangeCommits=true when corrections do not matter downstream, versus readChangeFeed=true when corrections must propagate and downstream must handle update_preimage/update_postimage pairs.
  4. Cut over: use a NEW checkpoint location plus startingVersion = last-processed + 1. A new CDF read without that starting bound returns the current snapshot as INSERTs; an existing checkpoint resumes its saved state.
  5. Archive rule: if CDF output is your audit trail, write it incrementally to a separate table — CDF is transient and ages out with the table.
“My bronze restatements kill my silver stream — is skipChangeCommits or a CDF read the right answer?”

If the restated values don't matter downstream — or a separate process handles them — skipChangeCommits=true is the docs' recommended default for workloads that don't use change data feeds: simple, appends only. If the corrected values must propagate, read the change data feed — the official comparison treats it as the robust path because your code handles every change-event type explicitly.

The price: downstream logic must handle update_preimage/update_postimage pairs instead of plain inserts, CDF sees only post-enablement changes, and the cutover needs startingVersion plus a fresh checkpoint location. What you must not do is leave a plain stream in place and hope the next restatement is the one that doesn't fail — the append-only contract guarantees it fails.

“I enabled CDF today — can I read last month's changes?”

No. Legacy change data feed “only records changes that occur after change data feed was enabled” — past changes are never captured, and an off-then-on gap stays unqueryable. If you need the history, enablement had to happen before the history did. Plan it on day one, not the day of the incident.

“Can I keep CDF as my permanent audit log?”

No — a change data feed is “not intended to serve as a permanent record of all changes to a table.” Old versions and their CDF age out with table retention; VACUUM deletes change data files. If you need permanent history, incrementally write CDF output into a separate table — a trigger(availableNow=True) stream with its own checkpoint does exactly that.

Use this when

  1. Upstream corrections — restatements, GDPR deletes, or late arrivals — must reach downstream.
  2. You need row-level auditability between versions through _change_type, _commit_version, and _commit_timestamp.
  3. An incremental silver-to-gold pipeline reads a table that sees MERGE, UPDATE, or DELETE.

Avoid this when

  1. The source is strictly append-only — a plain streaming read is simpler, and legacy CDF can add storage overhead in separate change data files.
  2. Corrections are handled elsewhere and downstream replays would be wrong — use skipChangeCommits.
  3. You need permanent history — CDF is transient and ages out with table retention; archive it yourself.
Assumptions: Legacy CDF path on a Databricks Runtime 15.4 LTS-class environment; Delta Lake tables (legacy CDF is Delta-only); automatic CDF needs DBR 19+ with a Unity Catalog-registered, row-tracking-enabled Delta table; skipChangeCommits needs DBR 12.2 LTS+.
Provenance: Last verified against official Databricks documentation: 2026-10-07. Correction history: first publication — no corrections.
RECOMMENDED LEARNING

Make change propagation an explicit design choice.

Build the Delta Lake and incremental-pipeline foundations behind resilient change handling.

BUILD THE FOUNDATION

Databricks fundamentals

Strengthen the platform basics behind Delta tables, streaming reads, jobs, and production data workflows.

Browse DataCamp Databricks courses →
BUILD FOR PRODUCTION

Data engineering workflows

Practice incremental pipelines, change-data patterns, and validation habits that turn this issue's tactics into repeatable practice.

Browse DataCamp data engineering →

Partner links: we may earn a commission if you enroll through these recommendations, at no extra cost to you.

Primary sources

Official documentation

  1. Change data feed — Databricks: CDF concepts; automatic and legacy flavors; per-table and session-default enablement; non-retroactivity; transient records; the _change_type, _commit_version, and _commit_timestamp schema; name conflicts; batch bounds; timestampGreaterThanLatestCommit and its tolerance flag; streaming snapshot replay; cutover; and storage behavior.
  2. Delta Lake streaming reads and writes — Azure Databricks: the append-only streaming contract; the four-approach comparison; skipChangeCommits semantics and recommendations; and legacy ignoreChanges behavior.

Practitioner case studies

  1. Databricks Community (October 1, 2026) — community-sourced, not official documentation. A DBR 15.4 LTS Unity Catalog user's silver stream failed after a monthly restatement UPDATE on bronze; the author weighs skipChangeCommits, a CDF read, and full refresh.
  2. Databricks Community (October 5, 2026) — community-sourced, not official documentation. A practitioner reports that a table without CDF falls back to full refresh for materialized-view refreshes and recommends checking delta.enableChangeDataFeed before wiring it.