ref(ddl): Improve DDL handling and ordering - #941
Draft
iambriccardo wants to merge 13 commits into
Draft
Conversation
iambriccardo
commented
Jul 28, 2026
| state_store.update_table_state(self.table_id, state.clone()).await?; | ||
| } | ||
|
|
||
| self.set(state); |
Contributor
Author
There was a problem hiding this comment.
This was a much needed correctness fix, so that we persist state in memory only after the store has acknowledged it.
iambriccardo
commented
Jul 30, 2026
| /// on append order or BigQuery ingestion-time tie-breaking. | ||
| fn bigquery_sequence_key(sequence_key: EventSequenceKey, internal_ordinal: u64) -> String { | ||
| format!("{sequence_key}/{internal_ordinal:016x}") | ||
| let commit_lsn = u64::from(sequence_key.commit_lsn); |
Contributor
Author
There was a problem hiding this comment.
I have just inlined this, since having 016x in the Display impl of EventSequenceKey made it too tied with BigQuery specifics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR makes schema-change replication reliably ordered across streaming, retries, table-sync handover, and process restarts.
It establishes a stable schema-version contract:
Event::Relationcarries the effective schema for subsequent rows without participating in DML sequence numbering; andTogether, these changes give destinations a monotonic schema timeline while preserving existing DML ordering and deduplication semantics.
Motivation
Previously, publication column-list changes could modify the effective downstream schema without producing a new snapshot ID. A destination could therefore observe one physical table snapshot with different replication masks, without a stable way to order those states.
Relationevents also carried DML sequence information even though PostgreSQL can emit them repeatedly across replication connections. This made DML ordinals depend on connection-local schema metadata rather than solely on source transactions.Finally, during table-sync handover, PostgreSQL may have already sent the relevant relation to the apply connection while the table-sync worker still owned the table. PostgreSQL does not necessarily resend that relation after ownership changes, so the apply worker needs a durable fallback to decode the first post-handover row safely.
What changed
Stable schema snapshots
ALTER TABLEoperations.ALTER PUBLICATIONchanges, including publication column-list changes.A publication-filter change snapshots the complete physical table schema even when that schema is unchanged. The logical message gives that snapshot a newer ID, while the following
Relationsupplies the new publication and replica-identity masks. Destinations therefore compare two ordered(snapshot ID, replication mask)states instead of two incomparable masks sharing one snapshot ID.For
ALTER PUBLICATION, snapshots are emitted for affected tables that are current members of the publication after the operation. Removing a table from a publication does not emit a new snapshot for the removed table.Relation and DML ordering
EventSequenceKeyfromEvent::Relation.EventSequenceKey(commit_lsn, tx_ordinal).Relationas connection-local schema metadata that establishes the active schema for following rows.Destinations use the relation’s snapshot ID and replication mask to identify schema state. DML sequence keys remain responsible only for ordering and deduplicating row mutations.
Decoder ownership and table-sync handover
The apply loop now tracks explicit per-connection decoding state:
WaitingForRelation { snapshot_id }means ETL knows which schema snapshot is required but does not yet have the relation masks needed to decode rows.WithSchemacontains the complete replicated schema needed for decoding.At table-sync completion,
SyncDonedurably stores:The apply worker can restore this decoder if its first post-handover DML arrives without PostgreSQL resending the relation. ETL rejects incomplete handovers instead of guessing historical masks.
SyncDonetoReadyFor a handover at LSN
H, a table transitions fromSyncDone(H)toReadyonly when:H;H; andWithSchemadecoding state.These conditions protect two separate guarantees:
Until all three conditions hold,
SyncDoneretains the durable decoder fallback.Destination schema safety
All built-in destinations apply the same conservative validation before destination DDL:
DML deduplication is deliberately not used to justify stale schema replay. A row watermark cannot prove that reverse or destructive DDL is safe.
Recovery from interrupted destination DDL remains destination-specific:
Applyingmetadata.Unsupported or ambiguous recovery states fail explicitly and may require table resynchronization. More sophisticated recovery can be added per destination without changing the source-side snapshot and relation contract.
Known limitations
Idle tables may remain in
SyncDoneIf no apply-owned relation or DML arrives after table-sync handover, the table may remain in
SyncDoneindefinitely.Initial copy and catch-up are already durably complete. Retaining
SyncDonepreserves the decoder needed if the first future row arrives without a preceding relation. That relation or row will materialize the apply-worker decoder and allow the table to becomeReady.DDL without a following relation during catch-up
A table-sync worker may decode DDL during catch-up without receiving a following relation when no DML occurs for that table.
The DDL snapshot contains the physical schema but not the exact historical publication and replica-identity masks. Reading the current PostgreSQL catalog would also be unsafe because it may already reflect later changes.
ETL therefore fails the table sync before persisting an incomplete
SyncDone. Retrying or resynchronizing the table after schema activity has settled is currently required.Destination recovery is intentionally conservative
Stale relations and unsupported interrupted
Applyingstates may require table resynchronization. This PR prioritizes explicit failure over reverse DDL or inferred recovery that could corrupt destination data.Backward Compatibility
The system remains backward compatible with the previous
SyncDoneschema. If an existingSyncDonestate lacks decoding, the system will simply be unable to install theSyncDonedecoding state for tuple decoding. This results in the same failure scenario as before, so it is not a regression; the system behaves exactly as it did previously. That existing behavior is addressed in this PR with the proper decoding state handover.