v0.1 pre-release · Apache-2.0 Start an evaluation

Concepts

Schema and DDL barriers

Developer · Operator How a schema change crosses the stream without exposing rows under a schema nobody agreed to.

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.

FieldMeaning
total_orderSource order across DDL and DML in one shared namespace.
operationBounded classification — currently add_column or other.
relationThe affected PostgreSQL relation.
statementThe source DDL statement, for target planning.
schema_fingerprint_before / _afterRelation fingerprint either side of the change.
target_auto_applyWhether a target applier may consider automatic execution.
release_gateMust 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.

The projection, and why it has its own checksum

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

ValueMeaning
compatibleThe change can be carried without target mapping.
requires_mappingThe target needs an explicit column or type mapping before it can apply.
destructive_or_ambiguousDropping, renaming or type-changing — replay, snapshots or target apply could break.
blocked_by_policyOtherwise 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 kindCompatibilityNote
add_nullable_columncompatibleOnly if the relation is already in dataset.tables; otherwise blocked_by_policy.
widen_typecompatibleSame condition.
increase_varcharcompatibleSame condition.
add_tabledependscompatible under unknown_table_policy: allow_compatible; blocked_by_policy under the default reject.
rename_columnrequires_mappingRename-like DDL is ambiguous without an explicit mapping.
rename_tablerequires_mappingSame.
drop_columndestructive_or_ambiguousCan break replay, snapshots or target apply.
narrow_typedestructive_or_ambiguousSame.
add_not_null_columndestructive_or_ambiguousSame.
change_primary_keydestructive_or_ambiguousAlters replica identity and idempotent apply semantics.
change_partition_keydestructive_or_ambiguousAlters partitioned-scale ordering and barrier semantics.
unknownrequires_mappingAnything the parser does not recognise needs operator classification.
The hard limit

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 fieldPurpose
barrier_id / barrier_scopeIdentity of this barrier and how widely it applies.
cdc_transaction_boundaryThe transaction boundary the barrier is pinned to.
row_visibility_mode / dml_after_barrier_heldWhether post-DDL rows are held from readers.
requires_global_partition_pauseWhether every partition lane must pause, not just the affected one.
sink_count / required_ack_count / ack_quorumHow many destinations must acknowledge, and under what quorum rule.
phasesOrdered phases, each with its own release condition.
release_gatesEach gate's required evidence and the condition that opens it.
release_blockersWhat 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.

On this page