Skip to content

[MSSQL]: Disable Automatic Schema change handling - #727

Open
stevensJourney wants to merge 24 commits into
mainfrom
sourcetable-metadata-mssql-capture-recording
Open

[MSSQL]: Disable Automatic Schema change handling#727
stevensJourney wants to merge 24 commits into
mainfrom
sourcetable-metadata-mssql-capture-recording

Conversation

@stevensJourney

@stevensJourney stevensJourney commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

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 SourceTable reconciliation and opaque sourceMetadata persistence 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 reports PSYNC_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_S1602 rather 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 reports PSYNC_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 SourceTable values. 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_metadata JSONB 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.

@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d695c57

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@powersync/service-module-mssql Minor
@powersync/service-module-postgres-storage Minor
@powersync/service-module-mongodb-storage Minor
@powersync/service-core Minor
@powersync/service-schema Minor
@powersync/service-image Minor
@powersync/service-module-convex Patch
@powersync/service-module-mongodb Patch
@powersync/service-module-mysql Patch
@powersync/service-module-postgres Patch
@powersync/service-module-core Patch
test-client Patch

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 rkistner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread modules/module-mssql/src/replication/CaptureReconciler.ts Outdated
Comment thread modules/module-mssql/src/replication/CaptureReconciler.ts Outdated
Comment thread modules/module-mssql/src/replication/CDCStream.ts Outdated
Comment thread modules/module-postgres-storage/src/types/models/SourceTable.ts
@stevensJourney stevensJourney changed the title (wip) feat: source table metadata tracking and MSSQL capture instance tracking feat: source table metadata tracking and MSSQL capture instance tracking Aug 3, 2026
@stevensJourney
stevensJourney marked this pull request as ready for review August 4, 2026 09:15
Comment thread .changeset/mssql-fixed-table-set.md Outdated
Comment thread .changeset/mssql-fixed-table-set.md Outdated
Comment thread .changeset/mssql-fixed-table-set.md
Comment thread modules/module-mssql/src/replication/CaptureReconciler.ts Outdated
Comment thread modules/module-mssql/src/common/MSSQLSourceTable.ts Outdated
@stevensJourney stevensJourney changed the title feat: source table metadata tracking and MSSQL capture instance tracking [MSSQL]: Disable Automatic Schema change handling Aug 6, 2026
@stevensJourney

Copy link
Copy Markdown
Collaborator Author

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.

  • Add a table: Create the table, enable CDC, add its exact qualified name to the sync config, and deploy. The new stream (sync config) snapshots the table before becoming active.
  • Change captured columns: Where SQL Server allows a rolling change, apply the DDL and create a second capture instance with the desired captured columns. Update explicit sync queries if needed, deploy a new stream (sync config with changes), wait for its snapshots to finish and for it to become active, and only then remove the old capture instance.
  • Make an identity-breaking change: For a primary-key change, identity-column rename, or another change that requires CDC to be disabled, disable CDC, apply the DDL, re-enable CDC, update the sync config if needed, and deploy a new stream. The old stream cannot adopt the replacement capture instance - this will result in downtime.
  • Drop a table: Prefer removing it from the sync config, deploying and activating the replacement stream, and only then dropping the source table.
  • Rename or drop/recreate a table: Update the config to express the intended table explicitly, ensure the resulting table is CDC-enabled with the expected schema and replica identity, and deploy a new stream.
  • No deployment is needed for changes that do not affect the captured row shape or replica identity, such as many index, constraint, or default changes. A non-identity column change also needs no PowerSync action if that changed column does not need to be replicated; otherwise use the rolling capture-instance workflow above.

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 temp-mssql-tests branch contains the full schema-change analysis and workflow, along with integration-test reproductions of the old behavior on main. Those tests deliberately assert the reproduced inconsistent states, so a passing test means the inconsistency was successfully reproduced. The risks depend on timing, replication lag, and the specific transition; they are possible under the old flow, but not every occurrence of the corresponding DDL necessarily produces an inconsistent checkpoint.


This comment was generated by prompting Codex with context and test generated in the temporary branch. The tests in the temporary temp-mssql-tests branch were generated with help from Codex 5.6 and reviewed for accuracy with Claude Opus 5.

rkistner
rkistner previously approved these changes Aug 6, 2026
Comment thread .changeset/mssql-fixed-table-set.md Outdated
- **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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment for the other cases listed here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .changeset/mssql-fixed-table-set.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants