The safety model

delta-engine mutates shared production tables, so its default posture is refusal: it only applies changes it can make safely in place, and everything else fails with a named rule before any SQL runs. This page explains the four layers that enforce that, and the scoping concept — managed aspects — that decides what a declaration is responsible for at all.

Four layers of protection

Layer

When it runs

What it catches

Declaration-time checks

Constructing a DeltaTable

Declarations that could never succeed — a nullable primary key, a typo’d property key

Validation rules

Every sync, after diffing live state

Transitions that are unsafe from this table’s current state — tightening nullability, narrowing a type

Managed aspects

Every sync, as part of validation

Drift outside what the declaration manages — never silently reconciled

Dependency blocking

Every sync, during resolution and execution

Foreign keys that can’t be satisfied — a cycle, a missing or mismatched reference — and tables whose dependency won’t reach its desired state this run

Declaration-time checks

Some declarations are invalid regardless of catalog state, so they fail immediately with a ValueError when the DeltaTable is constructed — long before a sync, typically at import time. This is deliberate: the earlier an impossible declaration fails, the closer the error is to the code that caused it. The full list is in safe-change rules.

Validation rules

Other changes are only unsafe in context: adding a NOT NULL column is fine on a new table but unsafe on an existing one with rows. These rules run on every sync, after the engine has read the live table and diffed it against the declaration. Each rule blocks a specific transition and its failure message names the rule, the affected column or table, and the way out — usually either a staged migration (backfill, then tighten outside the engine) or recreating the table out of band. The full rule table is in safe-change rules.

A validation failure means that table is untouched: no SQL ran, and re-syncing after fixing the declaration is always safe.

Managed aspects: what a declaration is responsible for

Every declaration manages a defined set of aspects of its table — column structure, comments, properties, tags, partitioning, and key constraints. For catalog state it can represent, the engine reconciles drift in managed aspects and refuses to proceed when an unmanaged aspect has drifted. It never silently reconciles something a declaration did not claim responsibility for.

The read boundary upholds this by failing closed: a column whose type the engine cannot model fails that table’s read (READ_FAILED) rather than being silently omitted from the observed state, so drift can never hide in a column the reader could not represent. See unsupported types.

There are three public scopes, selected by DeltaTable’s scope parameter:

Scope

Manages

Use for

"full" (the default)

Everything: columns, comments, properties, tags, partitioning, primary and foreign keys

Tables this declaration owns end to end

"metadata"

Comments, tags, and key constraints only

Rolling out governance metadata with a hard guarantee that no schema change can slip in

"tags"

Table and column tags only

Tag governance for tables owned elsewhere — including streaming tables, where tags are the only manageable aspect

See how to deploy metadata only for the metadata scope in practice, and tags for tag-only declarations.

Streaming tables make the scope boundary literal. Their definition is owned by a pipeline, so the engine reads one for tag governance only: the relation kind is discovered at read time, and a declaration that manages anything beyond tags fails validation (StreamingTableTagsOnly) before any SQL runs — even with zero drift. See safe-change rules for the invariant and tag a streaming table for the workflow.

Cross-table dependency blocking

The first three layers protect a table from its own declaration. The fourth protects it from the rest of the run: a table only executes when every table its foreign keys reference can be trusted to reach its desired state this sync.

A table blocked by this layer reports FOREIGN_KEY_FAILED, with one of five reasons in its failure message:

Failure reason

Fires when

UNRESOLVABLE_REFERENCE

A foreign key references a table that is not part of the sync

CYCLE

The table is part of a foreign-key dependency cycle

REFERENCED_COLUMNS_NOT_A_KEY

The referenced columns are not the referenced table’s primary key

REFERENCED_COLUMN_TYPE_MISMATCH

A foreign-key column’s type does not match the referenced column’s type on the table registered for the sync

BLOCKED_BY_FAILED_DEPENDENCY

A referenced table failed this run — at any of the layers above, or while executing

The first four are structural problems with the declarations themselves — validation in spirit, run separately only because they need to see every table in the sync at once. The last is failure propagation: the declaration is fine, but the state its foreign keys depend on won’t exist, so the table is blocked rather than executed.

The rule is uniform: if a dependency won’t reach its desired state this sync, its dependents don’t run either. Fix the upstream table and re-sync. How a sync works covers where resolution sits in the phase chain; foreign keys covers the declaration side.

Declared, observed, or both: aspect semantics

Within the managed aspects, what counts as drift depends on the aspect. The differences are intentional:

Aspect

Semantics

Properties

Exact-declaration: only declared keys are compared. Declaring a key with value None asserts its absence. A managed key set on the table but missing from the declaration fails validation rather than being ignored or unset.

Tags

Full-state: the declaration is the complete tag state. A tag on the table that is not declared is drift and will be removed.

Comments

Always managed: the declared comment (including “no comment”) is the desired state, so removing a comment from the declaration clears it on the table.

The property semantics are the most conservative because properties change engine behaviour — retention, change data feed, column mapping — where an unintended unset can be destructive. Tags and comments are pure metadata, so converging on the declaration is always safe. Details and examples: properties, tags, comments.

When the engine says no

A refused change is not a dead end; the failure message and safe-change rules name the resolution for each rule. The patterns are:

  • Stage the migration. Add the column nullable and backfill. The tighten itself is an out-of-band step (ALTER TABLE ... SET NOT NULL — the engine refuses to run it); once applied, declare nullable=False and the next sync sees no drift.

  • Recreate out of band. Type and partitioning changes require a rebuild; do it deliberately, then re-sync.

  • Fix the declaration. If the catalog is right and the declaration is stale, update the declaration — the engine treats the declaration as desired state, not automatically as correct.

To see what a sync would do before letting it touch anything, use a dry run.