[MSSQL]: Disable Automatic Schema change handling - #727
Conversation
🦋 Changeset detectedLatest commit: d695c57 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
rkistner
left a comment
There was a problem hiding this comment.
I'm happy with these changes overall. I did not check the comments on minLSN in detail.
Mentioned by Codex on the existing recovery path:
modules/module-mssql/src/replication/CDCStream.ts:811 tells operators to re-enable CDC, while modules/
module-mssql/src/replication/CaptureReconciler.ts:107 gives similar guidance. Re-enabling CDC creates
a replacement change table with a different object ID, which the pinned stream deliberately refuses to
adopt. The actionable recovery is restoring the original capture instance if possible, or deploying a
new replication stream. The current message can leave operators expecting an indefinitely stopped
stream to recover.
…fresh capture instances.
…s. Update diagnostics API wildcard handling.
|
The recommended MSSQL schema-change workflow is to treat a sync-config deployment as the consistency boundary. A replication stream keeps the exact table set and CDC capture-instance identities it selected when it started; it does not adopt schema or table changes in place. Tables must therefore be listed explicitly rather than selected with wildcards.
This sequencing keeps the old table/capture instance available while the replacement stream snapshots and activates whenever a rolling transition is possible. If a pinned capture instance or bound table is removed first, the existing stream stops and a fresh deployment is required. The This comment was generated by prompting Codex with context and test generated in the temporary branch. The tests in the temporary |
| - **Adding a table to replication:** deploy a new sync configuration. Table wildcards (`%`) are no | ||
| longer supported, so list each table explicitly, and every table must exist with CDC enabled when | ||
| replication starts. | ||
| - **Changing a table's columns:** deploy a new sync configuration. Replication continues against the |
There was a problem hiding this comment.
To understand the "Replication continues against..." sentence better - I assume that case happens when one doesn't deploy new sync config? If that's the case this would be less confusing if we start the sentence with "Without this replication continues..." or similar.
There was a problem hiding this comment.
Similar comment for the other cases listed here
There was a problem hiding this comment.
I assume that case happens when one doesn't deploy new sync config
It's not necessarily this. I agree we probably don't describe the relationship between sync config and replication streams well enough yet.
Essentially. When one deploys a sync config. That config is processed in a job which streams changes from some replication stream.
When making changes to a table's columns we recommend doing a sync config deploy. That will start a new job which runs against the new replication stream (in this case). The old replication job continues running using the old replication stream (the previous capture table). Connected clients continue getting updates from this stream. When the new sync config is in the ready state, we make it the active sync config - causing clients to get updates from here.
| with `PSYNC_S1603` — changes committed before the table went away may not have been read yet, so | ||
| continuing would commit past them and skip those rows. Already-replicated data for the table is | ||
| retained until the redeploy. | ||
| - **Disabling and re-enabling CDC on a replicated table:** deploy a new sync configuration as a new |
There was a problem hiding this comment.
"deploy a new sync configuration as a new replication stream" - I think I pointed this out previously. Maybe this is due to some internals leaking from our incremental reprocessing work, or us not explaining those mechanics well yet, but if I read this as a user I have no idea what I need to do about the "as a new replication stream" part of this sentence. For all user-facing messaging about this shouldn't we just say "deploy new sync config", and then optionally explain how the "new replication stream" fits in?
There was a problem hiding this comment.
👍 I've updated the terminology throughout. I've added context for Agents to refer to it as "sync config".
|
|
||
| Every configured table is resolved before streaming starts, which also backfills pins for legacy records. A table that does not exist or does not have CDC enabled fails with `PSYNC_S1602`. | ||
|
|
||
| Adopting a new capture instance requires a new replication stream. Keep the old instance available until the new stream has finished its snapshots and is active. |
There was a problem hiding this comment.
Is this something that we should also document in the "What this means when making schema changes:" section of the release notes? Something about old capture instances must remain until reprocessing completes if I understand this correctly?
And on that note, my AI previously pointed out that a similar restriction applies to dropping tables - apparently new sync config needs to be deployed first, then reprocessed, then table can be dropped?
There was a problem hiding this comment.
Yes, that's correct. I've updated the changeset entry to align more with the comment which mentioned this #727 (comment)
apparently new sync config needs to be deployed first, then reprocessed, then table can be dropped
Yes, that's correct.
Summary
This PR pins each MSSQL source-table binding to a specific CDC capture instance and keeps the replicated table set and captured schema fixed for the lifetime of a replication stream. This lets an existing stream continue using its original capture schema while a newly deployed stream binds to a replacement capture instance and snapshots the current source table.
MSSQL replication no longer handles source schema changes automatically. Table wildcards (
%) are therefore no longer supported: tables must be listed explicitly, and schema changes that affect replication require deploying the updated sync configuration as a new replication stream.The PR also adds generic, source-owned
SourceTablereconciliation and opaquesourceMetadatapersistence to MongoDB and PostgreSQL storage.MSSQL behavior
Why streams no longer adopt schema changes
SQL Server CDC does not include schema-change events in its replication stream. PowerSync can only discover them by polling database metadata, and that poll cannot be made atomic with source transactions or CDC reads. There is therefore no reliable source position at which PowerSync can say that one schema ended and another began.
Changing a stream's schema or table set in place could let it commit a checkpoint using metadata observed after the corresponding source changes. That can skip unread changes or expose a state that never existed atomically in the source database. MSSQL replication streams now keep their initial table set and captured schemas for their entire lifetime instead of trying to infer an unsafe transition boundary.
Capture-instance pinning
Each new source-table binding selects the newest available CDC capture instance and persists the object ID of its change table. Legacy bindings are assigned and persist the instance the previous selection logic would have used. Restarts restore that binding, while polling refreshes its current LSN range without changing its identity.
When a newer capture instance appears, PowerSync warns but the existing stream remains pinned to its original captured schema. If the pinned instance is removed while streaming, replication stops with
PSYNC_S1601; automatically adopting its replacement could silently change the captured columns without a safe transition boundary. If no capture instance is available when a job starts, initial table validation reportsPSYNC_S1602.Adding tables
Table wildcards are no longer supported because a matching table could appear between metadata polling and checkpointing, with no atomic position from which to begin replicating it. Every configured table must be listed explicitly, exist, and have CDC enabled before the stream can start. A table that is unavailable or not CDC-enabled stops replication with
PSYNC_S1602rather than being silently omitted.Adding a table requires deploying the updated sync configuration as a new replication stream so that the table is included in that stream's initial snapshot and consistency boundary.
Changing columns
Adding, removing, or changing source columns does not alter an existing CDC change table's captured schema. PowerSync warns when it observes a difference but continues reading the schema belonging to the pinned capture instance.
Adopting the new columns requires a replacement capture instance and a new replication stream. The new stream binds to the replacement capture instance and snapshots the current source table, while the active stream continues serving updates from the old instance.
Dropping or renaming tables
If an active replication stream detects that a replicated table was dropped or renamed, replication stops with
PSYNC_S1603. Continuing without it could advance the shared checkpoint past changes that remain unread from that table. Existing replicated data is retained until a replacement sync configuration is deployed. If the table is already absent when a job starts, initial exact-table validation reportsPSYNC_S1602.For all schema changes, keep the old tables and capture instances available until the replacement stream has completed its snapshots and become active.
Storage and reconciliation
resolveTables()now loads all persisted records overlapping a discovered source table and lets the source connector classify isolated clones as compatible or incompatible through a read-only TypeScript interface.Storage compares source metadata changes against its original hydrated records and rematerializes reconciliation results from its canonical
SourceTablevalues. Only source metadata changes are accepted from the connector; snapshot state, memberships, identity, and cleanup remain storage-owned even if connector code bypasses the read-only type boundary.Source metadata is nullable and opaque to storage. MongoDB v1/v3 persist it on source-table documents, and PostgreSQL adds a nullable
source_metadataJSONB column. Connectors without custom reconciliation retain the existing generic identity comparison.Testing
Coverage includes source-table reconciliation and clone isolation, metadata persistence in all storage implementations, capture-instance selection and restart restoration, legacy binding backfill, newer or missing capture instances, fixed table-set validation, wildcard diagnostics, and table/column schema changes.
AI usage: The implementation was planned and iteratively reviewed with Codex. Claude Opus implemented an initial plan, followed by manual review and further Codex-assisted refinements.