Concepts
Schema and DDL barriers
A schema change is the one event that can invalidate every assumption downstream simultaneously. Trellara's position is that DDL travels inside the transaction boundary that contained it, and that rows committed after it stay invisible until every destination has acknowledged the change.
Fingerprinting first
Live pgoutput relation metadata is fingerprinted and
pinned per relation. Envelopes carry schema_versions,
and a changed fingerprint stops capture before rows are assembled under
an unexpected schema. This is a fail-closed check, and one of the deterministic
scenarios in the failure matrix exists to prove it fires.
DDL inside the transaction
When a source transaction contains schema changes, they travel as
DdlEvent entries in the same envelope as the DML that
shared their commit boundary.
| Field | Meaning |
|---|---|
total_order | Source order across DDL and DML in one shared namespace. |
operation | Bounded classification — currently add_column or other. |
relation | The affected PostgreSQL relation. |
statement | The source DDL statement, for target planning. |
schema_fingerprint_before / _after | Relation fingerprint either side of the change. |
target_auto_apply | Whether a target applier may consider automatic execution. |
release_gate | Must be post_ddl_dml_release for automatic apply. |
DDL and DML share one total_order namespace, so a
duplicate order value between a row change and a DDL event is invalid — it would
make target replay ambiguous about which happened first.
Once the schema barrier releases, target apply may derive
dml_replay_after_ddl_barrier: a projection that strips
the DDL metadata and preserves the ordered DML. It carries its own
checksum precisely so post-DDL DML can never be confused with the original mixed
DDL/DML envelope. The original envelope remains the source of truth for audit and
replay ordering.
Three separate axes
These get conflated constantly, including in earlier drafts of our own material. They are independent.
Compatibility — what the change is
| Value | Meaning |
|---|---|
compatible | The change can be carried without target mapping. |
requires_mapping | The target needs an explicit column or type mapping before it can apply. |
destructive_or_ambiguous | Dropping, renaming or type-changing — replay, snapshots or target apply could break. |
blocked_by_policy | Otherwise applicable, but the configured policy forbids it. |
Decision — what the planner concluded
auto_apply, stage_then_apply,
manual_review, or block. The decision is a
pure function of the other two axes: blocked_by_policy blocks under every
apply mode, destructive_or_ambiguous blocks under every mode except
manual-review, requires_mapping always lands in
manual review, and only compatible can reach
auto_apply — and only when you asked for
auto-safe.
Apply mode — what you configured
The --apply-mode flag on the planning commands:
auto-safe, staged-rollout,
manual-review, or block-destructive.
The propagation policy layer expresses the same intent per change as
auto_apply, staged_rollout,
manual_approval_required,
block_unsupported or
shadow_plan_only.
What each change kind is classified as
Eleven kinds are recognised. The classification is in the code, not in policy you can loosen — the only lever you have is the apply mode, and it can never promote a kind above its compatibility.
| Change kind | Compatibility | Note |
|---|---|---|
add_nullable_column | compatible | Only if the relation is already in dataset.tables; otherwise blocked_by_policy. |
widen_type | compatible | Same condition. |
increase_varchar | compatible | Same condition. |
add_table | depends | compatible under unknown_table_policy: allow_compatible; blocked_by_policy under the default reject. |
rename_column | requires_mapping | Rename-like DDL is ambiguous without an explicit mapping. |
rename_table | requires_mapping | Same. |
drop_column | destructive_or_ambiguous | Can break replay, snapshots or target apply. |
narrow_type | destructive_or_ambiguous | Same. |
add_not_null_column | destructive_or_ambiguous | Same. |
change_primary_key | destructive_or_ambiguous | Alters replica identity and idempotent apply semantics. |
change_partition_key | destructive_or_ambiguous | Alters partitioned-scale ordering and barrier semantics. |
unknown | requires_mapping | Anything the parser does not recognise needs operator classification. |
Target SQL is generated for exactly three kinds, and only when the decision is
auto_apply or stage_then_apply:
add_nullable_column becomes
ALTER TABLE … ADD COLUMN IF NOT EXISTS;
widen_type and increase_varchar become
ALTER TABLE … ALTER COLUMN … TYPE. Every identifier is validated
against an identifier-safety check and quoted, and an unrecognised type spec produces no SQL at
all rather than a guess. For every other kind the planner emits a plan and no statement.
There is no configuration that turns this off.
The propagation barrier
Planning a change produces a propagation plan rather than a statement to run. The plan names the barrier and everything that has to happen before it opens:
| Plan field | Purpose |
|---|---|
barrier_id / barrier_scope | Identity of this barrier and how widely it applies. |
cdc_transaction_boundary | The transaction boundary the barrier is pinned to. |
row_visibility_mode / dml_after_barrier_held | Whether post-DDL rows are held from readers. |
requires_global_partition_pause | Whether every partition lane must pause, not just the affected one. |
sink_count / required_ack_count / ack_quorum | How many destinations must acknowledge, and under what quorum rule. |
phases | Ordered phases, each with its own release condition. |
release_gates | Each gate's required evidence and the condition that opens it. |
release_blockers | What is currently preventing release, named rather than implied. |
trellara schema ddl-plan --change add_nullable_column:public.sales.discount_code:text \ --apply-mode auto-safe --format json trellara schema ddl-barrier status --barrier-id <id> --format json
Why hold the rows at all
Because a sink that has not yet applied the DDL cannot correctly interpret a row written after it. Releasing post-DDL DML before every required sink acknowledges produces a destination that is silently wrong rather than visibly behind — and visibly behind is always the better failure. A target must acknowledge the DDL barrier before post-DDL DML is released.