Safe-change rules

The engine validates the computed diff before executing any SQL. These rules block changes that cannot be made safely in place. Each produces a validation failure and a PLANNING_FAILED status with a message naming the rule and the affected column or table. They are the validation-rules layer of the safety model, which explains how they fit alongside declaration-time checks and managed aspects.

Rule

What it blocks

How to resolve

NonNullableColumnAdd

Adding a NOT NULL column to an existing table

Add the column as nullable, backfill, set NOT NULL outside the engine, then declare nullable=False

NullabilityTighteningOnExistingColumn

Changing an existing nullable column to NOT NULL

Backfill existing NULLs, set NOT NULL outside the engine, then declare nullable=False

NonWideningColumnTypeChange

Changing a column’s declared data type outside the widening matrix (type widening)

Drop and recreate the table out of band, then re-sync

TypeWideningRequiredForTypeChange

A widening type change without delta.enableTypeWidening='true' declared

Declare the property (it may be set in the same sync as the widen)

PartitioningChangeNotSupported

Changing partitioned_by on an existing table

Rewrite/recreate it, or convert it to clustering out of band, then re-sync

PropertyTransitionNotSupported

A property transition that is unsafe in place — the catalog rejects it, or applying it would rewrite the table (e.g. delta.columnMapping.mode namenone rewrites every data file)

Update the declaration to match the catalog value

PropertyMustBeDeclared

A managed property set on the table but missing from the declaration

Declare it (or declare it None to remove it, where removal is possible)

ColumnMappingRequiredForDrop

A plan drops a column but the declaration lacks delta.columnMapping.mode='name'

Declare the property (it may be set in the same sync as the drop)

AmbiguousColumnRename

A declared rename whose old and new column both exist on the table

Remove the renamed_from hint and drop the old column in its own sync

PrimaryKeyReferencedByForeignKeys

Dropping or changing a primary key while foreign keys reference it (same-table FKs dropped in the same sync are exempt)

Sync the referencing tables without those foreign keys first, then change the key

Clustering is not a blocked change

Unlike partitioned_by, changing a table’s liquid clustering keys has no validation rule blocking it: Delta reconciles clustering keys with ALTER TABLE ... CLUSTER BY (...) (or CLUSTER BY NONE to remove them), so the engine plans this in place. PartitioningChangeNotSupported reflects delta-engine’s create-only partitioning model. Changing one partition specification to another requires a data rewrite, and although Databricks Runtime 18.1 and above can convert a partitioned table to liquid clustering with REPLACE PARTITIONED BY WITH CLUSTER BY, the engine does not model that conversion.

Re-clustering only affects data written after the change: existing files keep their old clustering layout until they are rewritten by a subsequent OPTIMIZE (optionally OPTIMIZE FULL to rewrite the whole table immediately). The engine issues the ALTER TABLE but does not run OPTIMIZE; query performance on old data improves only once you optimize.

Column renames

A rename is declared with renamed_from on the new column (see renaming a column). The differ relabels the observed column through the hint before comparing, so a rename emits a single rename action rather than a drop plus an add. The hint applies only when the old name is observed and the new one is not; once applied it is inert, so it is safe to keep as declaration history and correct on a fresh catalog (remove the hint if the declaration’s scope is later narrowed). If both names are observed the rename cannot apply and AmbiguousColumnRename blocks the sync.

Partitioning and clustering metadata follow a mapped column’s identity, so a layout key rename does not create layout drift. Primary and foreign keys involving the column are replaced explicitly: the plan drops each key before the rename and re-adds the declared key afterwards, rather than relying on the implicit drops Databricks performs during RENAME COLUMN. An inbound foreign key still blocks a primary-key rename via PrimaryKeyReferencedByForeignKeys.

Renaming requires delta.columnMapping.mode='name'. Downstream consumers are affected in ways the engine cannot see and does not manage: streaming reads need a schemaTrackingLocation to survive a rename, and change data feed has limitations on column-mapped tables. These are consumer concerns, documented rather than validated. CHECK constraints and generated columns are also outside the model: change dependent expressions first, or Databricks rejects the rename at execution.

Three further checks are scope invariants rather than rules — they define what a declaration is allowed to govern and always run, regardless of the rule set:

Invariant

What it blocks

How to resolve

UnmanagedAspectDrift

An unmanaged aspect (e.g. column structure) has drifted from the declaration in a restricted-scope sync

Sync the table fully, or update the declaration to match the live schema

MissingTableUnmanaged

The table does not exist but this definition does not manage table existence

Create the table out-of-band first, or manage it fully

StreamingTableTagsOnly

The observed table is a streaming table and the declaration manages more than tags

Declare it with scope="tags"; the table’s definition belongs to its owning pipeline

StreamingTableTagsOnly judges the declaration against the observed relation kind, not against drift, so it fires even when the streaming table is currently in sync. Comments and properties stay unmanageable on streaming tables deliberately: the pipeline definition owns them and a refresh can revert out-of-band changes, whereas Unity Catalog tags persist.

Declaration-time checks

Some invalid states are rejected before any sync — a ValueError when the DeltaTable (or a type) is constructed, because no catalog state could make them succeed:

Check

What it rejects

Nullable primary key

A primary key column declared nullable=True

Duplicate foreign keys

Two foreign keys over the same local columns

Foreign key type match

Local column types differing from the referenced primary key’s column types

Unmanaged property key

A property key outside DELTA_PROPERTY_POLICY (e.g. a typo)

Property value format

A value the catalog would reject: boolean keys (delta.enableChangeDataFeed, delta.enableTypeWidening) must be lowercase true/false, retention durations interval <n> <unit>, delta.dataSkippingNumIndexedCols an integer >= -1, delta.columnMapping.mode none/name

Column and struct field names needing column mapping

Special characters (spaces, ,;{}()=, tabs, newlines) in a column name or any nested struct field name (reported as a dotted path, e.g. payload.order id) without delta.columnMapping.mode='name' declared

Rename hint without column mapping

A renamed_from hint on any column without delta.columnMapping.mode='name' declared — RENAME COLUMN requires column mapping, and the hint is visible at declaration time (unlike a drop, which is judged at sync time)

Incoherent rename hint

A renamed_from naming the column itself, matching a still-declared column, duplicated across columns, or declared on a restricted scope that does not manage column structure

CDF-reserved column names

_change_type, _commit_version, _commit_timestamp while delta.enableChangeDataFeed is declared true

Tag limits

More than 50 tags on the table or a column, or a tag key or value over 256 characters

Decimal precision

Decimal precision above 38

Partitioning

Partition columns of complex type (Array, Map, Struct, Variant), or partitioning by every column

Clustering

More than four clustered_by keys, a clustering key of complex type (Array, Map, Struct, Variant), or declaring both partitioned_by and clustered_by on the same table — see limitations

Validation runs before any SQL executes. A failed validation means the table is unchanged.