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 |
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 |
|---|---|---|
|
Everything: columns, comments, properties, tags, partitioning, primary and foreign keys |
Tables this declaration owns end to end |
|
Comments, tags, and key constraints only |
Rolling out governance metadata with a hard guarantee that no schema change can slip in |
|
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 |
|---|---|
|
A foreign key references a table that is not part of the sync |
|
The table is part of a foreign-key dependency cycle |
|
The referenced columns are not the referenced table’s primary key |
|
A foreign-key column’s type does not match the referenced column’s type on the table registered for the sync |
|
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 |
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, declarenullable=Falseand 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.