Troubleshooting Guide

Endrias Bridge

Fixes for the most common issues in the Setup Wizard and Endrias Bridge: stuck scrolling, "Connection failed" messages, and ODBC connection-string errors.

📦 Version: June 2026 🖥 Platform: Windows Server / Windows 10+ 📄 Install Path: C:\MigrationTool

🚦 Pre-Flight Check Failed

Endrias Bridge runs a 3-step pre-flight check automatically when you click Run Migration. If step 1 or 2 fails the migration is aborted and the Migration Log shows the specific failure.

Reading the pre-flight output

--- Pre-flight check ---
  [1/3] Source connection … OK
  [2/3] Target connection … FAILED: (pyodbc.OperationalError) Login timeout expired
Pre-flight FAILED — fix connection errors above before retrying.

Step-by-step diagnosis

Step that failedError patternFix
1/3 Source connectionLogin timeout expiredCheck hostname / port; verify VPN is connected; check SQL Server firewall rule / security group inbound 1433
Login failed for user '…'Correct username/password on wizard Step 5; ensure SQL authentication is enabled on source SQL Server (ALTER SERVER CONFIGURATION … or SSMS → Security → SQL Server and Windows Authentication Mode)
No connection configuredWizard Step 5 not completed — fill in all Source DB fields and re-run
2/3 Target connectionSSL SYSCALL / certificate verify failedAdd TrustServerCertificate=yes to the target connection string, or install the RDS CA cert bundle. See SSL certificate error (AWS RDS).
Database '…' does not existEndrias Bridge creates the target DB automatically — confirm the service account has CREATE DATABASE or that the DB exists and the account has at least db_owner
3/3 CDC eligibilityCDC eligibility … DISABLEDWarning only — migration continues. To use CDC mode: EXEC sys.sp_cdc_enable_db on source; ensure SQL Server Agent is running.

Header status pill states after pre-flight

After a failure the header pill turns red → Pre-flight failed. After fixing the issue, re-run the migration — it will re-run pre-flight automatically.

🩺 Configuration Health Check Failures

Open Tools tab → Configuration Health Check → Run Health Check to run all 8 verifications. Each failed row (red ✗) contains a specific remediation hint in the Detail column.

Check-by-check remediation

CheckStatusCauseFix
Source connection✗ FailedConnection string invalid or server unreachableFix wizard Step 5 source fields; verify network path
Target connection✗ FailedTarget DB unreachable or auth failureFix wizard Step 5 target fields; check security group / firewall
Source permissions⚠ LimitedLogin not in db_datareaderALTER ROLE db_datareader ADD MEMBER [your_login] on source DB
Target permissions✗ FailedLogin cannot CREATE TABLE on targetGrant CREATE TABLE, INSERT, UPDATE, DELETE, ALTER to target login; or add to db_owner
CDC eligibility⚠ DisabledCDC not enabled on sourceUSE [source_db]; EXEC sys.sp_cdc_enable_db; — requires SQL Server Agent running and SQL Server 2008+ Enterprise/Developer/Evaluation or Azure SQL
Staging disk space✗ CriticalC:\ has < 5 GB freeFree disk space; or configure BACPAC output path to a larger volume with more available space
Staging disk space⚠ LowC:\ has 5–20 GB freeRecommended ≥ 20 GB for BACPAC exports; consider clearing temp files
ODBC Driver✗ MissingNo SQL Server ODBC driver installedDownload and install ODBC Driver 17 for SQL Server from Microsoft; restart Endrias Bridge after install
ODBC Driver⚠ Older versionODBC Driver 13 or earlierInstall Driver 17 or 18 alongside the older driver (both can coexist)
bcp.exe on PATH⚠ Missingbcp.exe not foundInstall SQL Server Command-Line Tools (msodbcsql + mssql-tools); add the install bin folder to the system PATH environment variable. Migration continues using PK-range copy fallback.

Health check does not start / button unresponsive

🔄 Update Checker Issues

The update checker (Tools tab → Check for Updates) queries api.github.com/repos/bilenbiruk24/Migrationtool/releases/latest.

Error / symptomCauseFix
Check failed: <URLError: timed out>No outbound internet / proxy blocking GitHub APIEnsure the staging host can reach api.github.com on port 443; configure proxy if required
Check failed: HTTP 403 / rate limitGitHub API rate limit (60 unauthenticated requests/hour per IP)Wait ~1 hour and try again; this is not a critical error
Shows "latest version" even after upgradeVersion constant in code not updatedVersion is hard-coded as v1.2.0 in _update_check_worker; update it after each release
Button appears but does nothingBackground thread silently erroredCheck Error Log tab; ensure urllib.request is available (standard library — should always be present)

🔵 Global Status Pill — All States

The status pill in the top-right corner of the header bar shows the current operation state at a glance.

Pill textDot colorMeaningWhat to do
IdleGreyNo operation runningNormal — ready to start
Migrating…AmberMigration in progressWait; monitor Migration Log
Pre-flight failedRedPre-flight check failedSee Pre-flight check failed; fix and re-run
Migration completeGreenMigration finished successfullyVerify row counts; proceed to Phase 4 hardening
Migration failedRedFatal exception during migrationCheck Migration Log for FATAL line; see Health Check to diagnose
Streaming (…)GreenCDC replication stream activeNormal — stream is running
Schema drift detectedAmberSource schema changed mid-streamSee CDC schema drift; acknowledge drift and restart stream
Health check running…AmberHealth check in progressWait for results (usually < 10 s)
Ready to migrateGreenAll 8 health checks passedProceed with migration
Health: N failedRedOne or more health check failuresReview health check Treeview; remediate failed rows
Health: N warningsAmberNon-critical health check warningsReview warnings; migration may still work

📖 About This Guide

This guide collects fixes for the issues most commonly hit while filling in Step 5 — DB Migration of the Setup Wizard and while using the Endrias Bridge (Assessment / Migration / Schema Objects / Tools tabs). For a full walkthrough of every screen, see Endrias Bridge_User_Guide.html.

Quick Checklist

Before digging into a specific error, run through this list — it resolves the large majority of reported issues:

  1. Re-type, don't paste, the Host/IP and Database fields

    Pasted text can carry invisible characters (trailing spaces, line breaks, non-breaking spaces) that look identical to normal text but break connections. If a field was pasted, click into it and re-type the value by hand.

  2. Put exactly one value per field

    The Host/IP field takes only a hostname or IP address (e.g. localhost or 10.0.0.5) — never a full connection string, URL, or key=value;key=value fragment copied from another tool.

  3. Use "Test Connection" first

    On Step 5, the Test Connection button next to each server checks that the host/port is reachable over the network before you try a full Assessment or Migration.

  4. Check the Migration tab log for the "Connection:" line

    When Assessment, Load Tables, or Migration fails, the Migration tab's log panel prints a Connection: ... line showing exactly what was read from each Step 5 field (password excluded). See Reading the Migration Log.

📊 Health Dashboard — Common Issues

CPU % shows 0 or never updates

Cause: psutil.cpu_percent(interval=None) always returns 0.0 on its very first call — it needs a prior baseline to compute a delta. Fixed in v1.2.1 by switching to interval=0.1 and removing the silent except Exception: pass wrapper in _hd_tick() that swallowed errors before they could surface.

If the resource panel is missing entirely: pip install psutil then restart the app.

Sparklines blank / not visible

Cause: winfo_width() or 200 evaluates to 1 (truthy) before the canvas is fully laid out. Fixed in v1.2.1. If charts still appear blank, resize the window slightly or switch tabs and back to force a redraw.

Disk Space shows "query error" or 42000

ErrorCauseFix
ODBC format errorOld code passed SQLAlchemy URL directly to pyodbc.connect()Update to v1.2.1 — _to_odbc() helper strips and URL-decodes the ODBC string first
Disk shows N/A for all SQL ServerType check used db_type != 'mssql' but wizard returns 'Azure SQL Database' etc.Update to v1.2.1 — now checks _SQLSERVER_FAMILY set
42000 permission deniedsys.dm_os_volume_stats needs VIEW SERVER STATE; Azure SQL PaaS blocks itv1.2.1 falls back through 3 levels automatically. For free-space on PaaS: GRANT VIEW DATABASE STATE TO [login];
Shows "allocated only"User lacks VIEW DATABASE STATE on Azure SQL PaaSGrant VIEW DATABASE STATE or accept allocated-only display

👤 Users & Logins Tab — Common Issues

Scan returns 0 users

The scan excludes system principals (guest, dbo, sys, public, INFORMATION_SCHEMA, MS policy logins). Verify directly on source: SELECT name, type_desc FROM sys.database_principals WHERE type IN ('S','U','G','E','X')

Apply fails with "User already exists"

Run Compare with Target first. Uncheck users shown as ✅ Exists — only apply users shown as ❌ Missing.

Azure AD user DDL fails on target

CREATE USER … FROM EXTERNAL PROVIDER fails if target is in a different Azure AD tenant. These users are marked ⚠ Manual Action Required. Add them through the Azure Portal instead.

Role membership not applied

Apply runs ALTER ROLE [role] ADD MEMBER [user] for each role. If the role doesn't exist on the target yet, that step is silently skipped. Create the roles first, then re-apply.

🖱️ Can't Scroll / Page Stuck in the Middle

On Steps 4, 5, and 6, and on the Migration tab's table list, content is taller than the window and scrolls inside a panel. If the mouse wheel or trackpad does nothing — or the view appears "stuck" partway down with no way to reach fields below it (most commonly the Target Database box on Step 5) — try the following:

  1. Hover directly over the panel before scrolling

    Scrolling is scoped to whichever panel the mouse pointer is currently over. Move the cursor over the Source/Target Database boxes (not the window chrome or another panel) before using the wheel/trackpad.

  2. Drag the scrollbar thumb on the right edge

    Every scrollable panel has a vertical scrollbar. Dragging it always works even if wheel/trackpad scrolling is being intercepted by another window.

  3. Resize or maximize the window

    A larger window shows more fields without scrolling at all — maximize the Endrias Bridge window or drag its bottom edge down.

  4. Restart Endrias Bridge

    If you're on an older build, restarting picks up the latest scrolling fixes. Note that restarting clears the wizard form — re-enter Steps 1-5 afterward.

💡
As of this build, mouse-wheel scrolling is bound per-panel (so the panel under the cursor scrolls, not whichever one happened to bind last), and toggling "Skip database migration" on Step 5 no longer freezes the scroll region. If scrolling is still stuck after restarting, it's most likely the window is simply too small — try resizing it first.

🌐 "Connection failed ... [Errno 11001] getaddrinfo failed"

This appears from the Test Connection button on Step 5. It means the operating system could not resolve the Host/IP value to a network address — even for values that look correct, like 127.0.0.1.

Most common cause: invisible whitespace

A trailing space, tab, or line break copy-pasted into the Host/IP field is invisible in the text box but makes 127.0.0.1 (for example) into 127.0.0.1  or 127.0.0.1\n — which the network layer cannot resolve at all, producing exactly this error.

💡
Fix: Click into the Host/IP and Port fields, select all (Ctrl+A), delete, and re-type the value by hand. As of this build, Step 5 also strips leading/trailing whitespace automatically before testing — but re-typing rules out any other hidden characters too.

Other causes

SymptomFix
Host is a hostname that doesn't exist on this network (typo, VPN not connected, DNS not configured) Try the server's IP address instead, or confirm DNS/VPN connectivity from a command prompt with ping <host>.
Host/IP field contains http://, https://, or tcp: prefixes Remove the prefix — enter only the bare hostname or IP, e.g. myserver.database.windows.net not tcp:myserver.database.windows.net.
Port field is non-numeric or contains extra text Enter only the port number, e.g. 1433 for SQL Server or 5432 for PostgreSQL.

⚠️ "Invalid connection string attribute (0)"

This is an ODBC driver / driver manager error (SQL Server connections only), shown with a SQLAlchemy "Background on this error" link. It means the connection string built from Step 5's Source/Target Database fields could not be parsed at all — it never even attempts to reach the network. The cause is almost always something in one of the Step 5 text fields, not the database server itself.

Cause 1 — A full connection string was pasted into Host/IP

If you copied a connection string from another tool (SSMS, Azure Portal, an app's appsettings.json, etc.) — something like:

# Looks like this:
Server=tcp:myserver.database.windows.net,1433;Database=AppDb;User Id=admin;Password=...;

...and pasted the whole thing into the Host/IP field, the ; and = characters land inside the connection string's SERVER= attribute and corrupt it.

💡
Fix: The Host/IP field takes only the server name or address — in the example above, that's just myserver.database.windows.net. Put the database name (AppDb) in the Database field, and the username/password in their own fields. As of this build, Endrias Bridge detects ;/=/{/} in the Host/IP field and shows a clear "Host/IP field contains characters that are not valid in a hostname" error instead of the cryptic ODBC message.

Cause 2 — Invisible whitespace in any Step 5 field

Same root cause as the getaddrinfo error above, but for the ODBC path: a stray newline or space pasted into Host, Database, Username, or Password gets embedded raw in the connection string and the driver can't parse it. Fix: re-type (don't paste) each field. As of this build, all five fields (host, port, database, username, password) are stripped of leading/trailing whitespace automatically.

Cause 3 — Special characters in the password

Passwords containing @ : / % ; = { } are automatically encoded/escaped so the ODBC driver and SQLAlchemy URL parser don't misread them as delimiters. You do not need to avoid these characters or escape them yourself — type the password exactly as it is.

Still seeing it after re-checking Host/IP and re-typing every field?

Go to the Migration tab and click Load Tables (or run Assessment). The log panel will print the exact error and a Connection: ... line showing every Step 5 field as it was actually read (with the password omitted) — see Reading the Migration Log. That line will reveal hidden characters (shown as '\n', '\xa0', etc.) that the form itself can't display.

🔑 Login / Authentication Failed

If Test Connection succeeds (the port is reachable) but Load Tables or Assessment fails with a login/auth error, the network path is fine — the username or password is being rejected by the database server itself.

Error containsLikely cause
Login failed for user '...' (SQL Server) Username or password is incorrect, or the SQL Server login doesn't have access to the database named in the Database field. Verify the login in SQL Server Management Studio / Azure Data Studio with the same credentials.
password authentication failed (PostgreSQL) Username or password is incorrect for the target Postgres role, or the role lacks CONNECT privilege on the database.
Cannot open database "..." requested by the login The Database field name doesn't match an existing database on that server, or the login isn't mapped to it. Double-check spelling and case.
⚠️
Never paste passwords or connection strings into chat with an AI assistant. If a credential has been shared anywhere outside this application, rotate it.

🧩 ODBC Driver Not Found

SQL Server connections require the ODBC Driver 17 for SQL Server to be installed on the machine running Endrias Bridge. If it's missing, errors mention Data source name not found and no default driver specified or IM002.

ℹ️
Install "Microsoft ODBC Driver 17 for SQL Server" (or 18, which is backward compatible) from Microsoft's website, then restart Endrias Bridge.

🧊 GUI Shows "Not Responding" During Migration

Windows marks the MigrationBridge window as "Not Responding" (title bar greyed out, can't click anything) while a migration is running on a large database such as acme_erp. The migration is still running correctly on a background thread — it is not crashed. The problem is that the UI queue becomes backlogged.

Root cause

The copy loop posts 3 queue messages to the UI on every batch (mig_log, progress, mig_progress). At a 100,000-row batch size on a 200-table database like acme_erp, that is potentially hundreds of messages queued faster than the UI poll loop (which ran at 50 items per 100 ms tick) could drain them. While the backlog builds, Tkinter cannot process window paint or input events and Windows declares the window unresponsive.

💡
Fixed in this build. Two changes:
  1. Progress messages are now emitted every 10 batches instead of every batch (every 1,000,000 rows instead of every 100,000 rows at the current chunk size). The last partial chunk of each table always emits to ensure the final count is logged.
  2. The UI poll loop now drains 200 items per tick instead of 50, so even if the queue builds up briefly it clears 4× faster.
Result: the UI stays responsive throughout long migrations, progress bar and log still update visibly, and "Not Responding" no longer appears.

If you see this on an older build

The migration is not stuck — leave it running. Do not kill the process. Killing MigrationBridge mid-copy leaves the target tables partially loaded; you then need to run Resume (after a rebuild) to complete them. If you have already killed the process, see the Resume section in the documentation for how to continue without re-migrating completed tables.

⚠️
Rebuild MigrationBridge.exe with PyInstaller to pick up both UI fixes before the next acme_erp run.

🔐 SSL Certificate Error — AWS RDS for SQL Server

All tables fail to connect with an error like:

[08001] SSL Provider: The certificate chain was issued by an authority that is not trusted.

This happens when the Target Database Type in Step 5's dropdown is set to Azure SQL Database instead of AWS RDS for SQL Server. Azure SQL Database uses a CA-signed certificate (TrustServerCertificate=no), while AWS RDS for SQL Server uses a self-signed certificate — so the connection string must set TrustServerCertificate=yes for RDS.

💡
Fix: On Step 5, change the Target DB Type dropdown from Azure SQL Database to AWS RDS for SQL Server. No code change is required — Endrias Bridge sets TrustServerCertificate automatically based on the selected DB Type.

🗂️ CREATE SCHEMA Syntax Error on SQL Server / RDS

All schemas fail to create with:

Incorrect syntax near the keyword 'IF'. [SQL: CREATE SCHEMA IF NOT EXISTS myschema]

CREATE SCHEMA IF NOT EXISTS is PostgreSQL syntax. SQL Server (on-prem, Azure SQL Database, and AWS RDS) does not support it. The fix uses a T-SQL pattern instead:

-- Correct T-SQL pattern (used by db_schema.py for mssql dialect)
IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = N'myschema')
    EXEC('CREATE SCHEMA [myschema]')
💡
This was fixed in migrationbridge/gui/db_schema.pyensure_schemas_exist() now branches on engine.dialect.name == 'mssql' and uses the T-SQL pattern for all SQL Server family targets (SQL Server, Azure SQL Database, Azure SQL Managed Instance, AWS RDS for SQL Server). If you still see this error, clear the __pycache__ directories and restart the GUI — see Code change not taking effect.

♻️ Code Change Not Taking Effect (Stale __pycache__)

After editing a Python source file, the GUI still behaves as if the old code is running. This happens because Python caches compiled bytecode in __pycache__ folders — the old .pyc file continues to be used even though the .py source was changed.

⚠️
You must fully close the Endrias Bridge GUI before clearing the cache — if any Python process still has the files locked, the clear won't help and the old bytecode will be reloaded on the next start.
  1. Close the GUI completely

    Close the Endrias Bridge window and confirm no Python process is still running (check Task Manager).

  2. Clear all __pycache__ directories

    Run this PowerShell command from the MigrationTool install directory:

Get-ChildItem -Path C:\MigrationTool -Recurse -Filter '__pycache__' | Remove-Item -Recurse -Force
  1. Restart the GUI

    Launch Endrias Bridge again. Python will recompile all source files from scratch, picking up the updated code.

⚠️ Verification Warnings After Schema Creation (Non-Fatal)

After creating tables you see lines like:

WARNING [SomeTable]: verification error - WARNING [AnotherTable]: verification error - Result: all 144 table(s) OK

These are not errors. The verification step uses SQLAlchemy reflection to confirm each table was created. On schema-qualified tables (e.g. HumanResources.Employee) SQLAlchemy's reflection can fail silently for some SQL Server / RDS configurations — but the tables themselves exist on the target and the data copy proceeds normally.

💡
The summary line Result: all N table(s) OK at the end confirms the DDL succeeded. If you see individual WARNING lines but the summary says OK, the migration is healthy and you can ignore them.

🔄 CDC System Tables Cannot Be Created on AWS RDS

During migration you may see failures for tables named cdc.captured_columns, cdc.change_tables, cdc.ddl_history, cdc.index_columns, and cdc.lsn_time_mapping.

The cdc schema is a SQL Server system schema reserved for Change Data Capture. On AWS RDS for SQL Server, this schema cannot be created manually — it only exists when CDC is enabled on the RDS instance by the database administrator.

ℹ️
These 5 tables are CDC internal metadata tables, not application data. If your source database had CDC enabled and those tables appear in the migration list, you can safely exclude them — or enable CDC on the target RDS instance and they will be created automatically by SQL Server.

🕐 Temporal Tables: System-Versioning Not Re-Enabled

After a full migration the log shows lines like:

salesdata.SomeTable_HISTORY: system-versioning FAILED - name 'text' is not defined (run manually from Schema Objects tab)

This affected all temporal tables in the same run and was caused by a missing from sqlalchemy import text import inside the _copy_schema_objects function. The temporal tables and their history tables were created and data was copied — only the final ALTER TABLE … SET (SYSTEM_VERSIONING = ON …) step failed.

💡
Fixed in this build. The import is now present and the SYSTEM_VERSIONING = ON ALTER is applied automatically at the end of every full or data-only migration for same-family targets (SQL Server family). If you have a database migrated before this fix, open the Schema Objects tab and use Re-apply Schema Objects to re-run the temporal re-enable step without re-migrating data.

Workaround before rebuild

Apply system-versioning manually with the script exported from the Schema Objects tab, or run the following pattern for each affected table:

-- Re-enable system-versioning manually after migration
ALTER TABLE [schema].[TableName]
    ADD PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]);
ALTER TABLE [schema].[TableName]
    SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [schema].[TableName_HISTORY]));

📃 sysname Column Type — CREATE TABLE Fails on RDS

sysname is a SQL Server built-in alias for nvarchar(128). On Azure SQL Database it can appear on user tables (typically for columns like PublishedBy). AWS RDS for SQL Server does not allow users to create columns of type sysname, so the schema-copy step would fail with:

Schema: salesdata.AgeBasedRate FAILED - Column, parameter, or variable #6: Cannot find data type sysname.

Root cause

SQLAlchemy reflects sysname as a custom type that round-trips back to the DDL string sysname. The existing cross-type mapping (retype_azure_for_rds()) did not handle this alias.

Fix (Change 43A)

A detection block was added to retype_azure_for_rds() in db_types.py: before DDL is emitted, any column whose reflected type string is sysname is remapped to NVARCHAR(128). The migration log records a NOTE line for each affected column.

Affected databases

QA_acme_erp (multiple schemas), QA_acme_custom (customdata). Any Azure SQL database with sysname columns is affected.

Temporal Table INSERT Error 13575 (Generated Always Column)

SQL Server refuses writes to SysStart / SysEnd period columns while SYSTEM_VERSIONING = ON. On a resume run the schema-objects step from the previous run has already re-enabled versioning, so every INSERT into a temporal table fails with:

salesdata.AgeBasedRate: ERROR — Cannot modify generated always column 'SysStartTime'. (Error 13575)

Root cause

After a full migration _copy_schema_objects() sets SYSTEM_VERSIONING = ON on all temporal tables. If data copy is then re-run (resume or repeat), the period columns are no longer writable.

Fix (Change 43B)

The new _disable_temporal_versioning() method is called in _do_migration() before the bulk copy step. For each temporal table currently ON on the target it issues ALTER TABLE ... SET (SYSTEM_VERSIONING = OFF). _copy_schema_objects() then re-enables versioning after the copy.

Manual remediation (before fix)

If you hit this error on an older build, disable versioning manually before running Resume:

-- Run for each temporal table that fails
ALTER TABLE [salesdata].[AgeBasedRate] SET (SYSTEM_VERSIONING = OFF);
-- ... repeat for each temporal table ...
-- After Resume completes, use Schema Objects → Re-apply to re-enable.

📅 datetime.max / DateTime.MaxValue — ODBC Error 22007 (Invalid Date Format)

SQL Server applications often store 9999-12-31 23:59:59.9999999 (DateTime.MaxValue in .NET) as a "never expires" sentinel. Python's ODBC Driver 17 reads this as datetime.datetime(9999, 12, 31, 23, 59, 59, 999999) — Python's datetime.max. When the tool tries to INSERT this value back into a SQL Server datetime2 column the driver fails:

AspNetUsers: ERROR — pyodbc.DataError ('22007', 'Invalid date format (0) (SQLParamData)')

The existing safeguard in convert_value() only clamped values with year > 9999. Because datetime.max has year = 9999 it passed the guard and reached ODBC, where the sub-microsecond fractional encoding at the year-9999 boundary overflows the datetime2(7) wire format.

A cascade effect follows: FK-child tables (e.g. AspNetUserRoles, TeamUserLinks) then fail with IntegrityError 23000 because the parent rows were never written.

Affected columns / tables

Any column storing DateTime.MaxValue as a sentinel, including LockOutDateTime, SessionExpiresOn, Timestamp audit columns (AppIdentity, AppAudit, AppConfig, etc.).

A related edge case: datetime.datetime(1, 1, 1, 0, 0) (DateTime.MinValue) can fail on legacy datetime columns because SQL Server's datetime minimum is 1753-01-01.

Fix (Change 44 + Change 46)

Two clamp blocks in convert_value() in db_types.py:

💡
If you see error 22007 and the bound parameter already shows 997000 microseconds, you are on Change 44 but not yet Change 46. Update to Change 46 (clamp target = 0 µs) and Resume Migration.

After applying both changes, re-run with Resume Migration. The FK-child IntegrityErrors will automatically disappear once the parent tables succeed.

🔄 CDC / DMS System Tables Processed During Resume

A resume run that uses a table selection from an older (unpatched) "Load Tables" click may still contain cdc.*, *_CT, attrep_*, or systranschemas entries. Before this fix the resume row-count loop would try to count rows on these tables — which don't exist on the RDS target — producing a wall of errors like:

cdc.captured_columns: not on target yet — will migrate cdc.change_tables: not on target yet — will migrate ERROR: Invalid object name 'cdc.captured_columns'.
💡
Fixed in this build. The resume path now skips any table whose schema is cdc, whose name ends with _CT, starts with attrep_ / _attrep_, or equals systranschemas — matching the same exclusion filter already applied by Load Tables.

If you are on an older build, simply click Load Tables again after the code is updated — the refreshed list will exclude CDC/DMS artifacts. Then start a new full migration (not resume) from the clean list.

🔢 "Converting decimal loses precision" Error

During data copy some tables fail with:

ERROR [salesdata.CustomCodeCpt_HISTORY]: ('HY000', 'Converting decimal loses precision')

This is a pyodbc fast_executemany quirk. When fast_executemany=True is enabled (the default for SQL Server targets to achieve 10-100× batch speed), pyodbc prepares the INSERT statement by inferring the decimal parameter type from the first row of the batch. If a later row has more significant digits (e.g. first row has Decimal('12.88') → inferred as DECIMAL(4,2), but a subsequent row is Decimal('18532.51') → needs DECIMAL(7,2)), pyodbc throws this error for the whole batch.

Affected tables typically have a decimal / numeric column where the integer portion varies widely between rows (e.g. rate tables, fee tables, financial detail tables).

💡
Fixed in this build. The batch error retry logic now catches 'loses precision' and 'converting decimal' alongside the existing truncation errors. When triggered, the batch is retried row-by-row using a non-fast_executemany connection — all rows land correctly, with individual row errors logged for inspection. No data is lost.

Tables commonly affected (Azure SQL → AWS RDS)

🔑 Sequences Missed on Resume or Data-Only Runs

When a table's DEFAULT constraint references a SEQUENCE object (i.e. DEFAULT (NEXT VALUE FOR [schema].[SeqName])), the sequence must exist on the target before any INSERT can succeed. In previous builds, ensure_sequences_exist was called only inside the if do_schema: block — so resume runs and data-only passes (which set do_schema=False) would skip it entirely, causing INSERT failures like:

ERROR: Cannot find object "dbo.MySequence" because it does not exist or you do not have permissions.
💡
Fixed in this build. Sequence creation now runs before the data copy regardless of whether schema creation was requested. On a full migration (schema + data) sequences are created once inside the schema phase; on resume / data-only runs they are created immediately before the data copy, with a Sequence [schema].[name]: created on target log line for each newly created sequence and no action taken for sequences that already exist.
ℹ️
This fix applies only when the target is a SQL Server family engine (SQL Server, Azure SQL Database, AWS RDS for SQL Server). Sequences are a SQL Server feature; other target engines are not affected.

🔌 TCP Drop Mid-Batch (Error 10054 / 08S01)

During a long-running migration you see a line like:

ClaimEnvelope: ERROR (PK-range) — ('08S01', '[08S01] [Microsoft][ODBC Driver 17 for SQL Server]TCP Provider: An existing connection was forcibly closed by the remote host. (10054)')

This is a transient network error — Azure SQL (or the network path to it) closed the TCP socket. Common causes: Azure SQL DTU throttling, a brief network blip, or Azure's idle connection timeout during a slow batch.

💡
Fixed in this build. copy_table_by_pk_range() now automatically retries the failed batch up to 5 times with exponential back-off (5 s → 15 s → 30 s → 60 s → 120 s). The engine connection pool is flushed (engine.dispose()) before each retry so a fresh TCP connection is opened — not the broken one. You will see warning lines in the migration log:
ClaimEnvelope: transient error on source read, retrying in 5s (attempt 1/5)...
ClaimEnvelope: transient error on source read, retrying in 15s (attempt 2/5)...
If all 5 retries fail, the table is marked ERROR and the migration moves to the next table — click Resume Migration afterward to continue from the last saved PK checkpoint.

If you're on an older build (no auto-retry)

The tool skips the table when this error occurs. The table is partially copied — its PK checkpoint was saved up to the last successful batch. Click Resume Migration to continue from that checkpoint.

Reducing TCP drop frequency

CauseFix
Azure SQL DTU exhaustion (throttling) Scale the source Azure SQL tier up during the migration window, or reduce Table concurrency to 1 to lower read pressure on the source
Azure SQL idle connection timeout (30 min) Already handled by engine.dispose() on retry — the pool creates a fresh connection. No action needed.
VPN / network path instability Run MigrationBridge from an Azure VM (same region as source) or an EC2 instance (same region as RDS) to eliminate VPN hops

🖥 Keeping the VM Active (No Sign-Out / Sleep)

A migration running on a Windows VM will stall or terminate if the VM signs out, sleeps, or the session is disconnected. Even if the tool has the TCP-drop auto-retry, a session sign-out kills the Python process entirely — the migration stops and no checkpoint is saved for the current batch.

Problem: VM has no internet — how to keep it alive?

Internet access is not required. The VM only needs network connectivity to the source Azure SQL and the target RDS — which you already confirmed works. The steps below are all local Windows settings.

Step 1 — Disable sleep and monitor timeout (run as Administrator)

Open Command Prompt as Administrator on the VM and run:

:: Prevent standby and screen-off while on AC power
powercfg /change standby-timeout-ac 0
powercfg /change monitor-timeout-ac 0

:: Optional: prevent hibernate as well
powercfg /change hibernate-timeout-ac 0

These settings survive a RDP disconnect — the VM stays awake even after you close your remote session. Restore after migration completes:

:: Restore to 30-minute standby after migration
powercfg /change standby-timeout-ac 30
powercfg /change monitor-timeout-ac 10

Step 2 — Prevent RDP session from being signed out

By default, Windows Server terminates disconnected RDP sessions after a timeout. Disable that policy for the duration of the migration:

:: Via Group Policy (run gpedit.msc) — or via registry directly:
:: Computer Configuration → Administrative Templates →
::   Windows Components → Remote Desktop Services →
::   RDP Session Host → Session Time Limits →
::     "Set time limit for disconnected sessions" → Disabled

:: Registry equivalent (run as Administrator):
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxDisconnectionTime /t REG_DWORD /d 0 /f

Step 3 — Run the tool in a detached session (most robust)

The most reliable approach on a VM with no direct monitor is to launch the migration tool inside a Windows Service or use Task Scheduler to run it under the SYSTEM account — the process then survives all RDP disconnects and session changes.

Alternatively, use tscon to detach your RDP session without logging off (preserves the desktop session):

:: Find your session ID (look for your username):
query session

:: Detach session 2 (replace 2 with your session ID) — keeps it running:
tscon 2 /dest:console
💡
AWS EC2 tip: If the staging host is an EC2 Windows instance, enable Session Manager (SSM) — it gives browser-based shell access that doesn't time out and doesn't depend on RDP port 3389 being open. The migration process stays alive even if your browser closes.

Verify the tool is still running after reconnect

After reconnecting to the VM, check the migration log is still active:

:: Check if migrationbridge process is running:
tasklist | findstr python

:: Tail the main log to confirm it is still writing:
powershell Get-Content C:\Windows\Temp\migrationbridge.log -Tail 20

If the process is gone, the migration stopped. Check the log for the last checkpoint written, then click Resume Migration — the tool will continue from the last saved PK.

Index Disable / Rebuild — How It Speeds Up Bulk INSERT

When rows are INSERTed into a SQL Server table that has non-clustered indexes, the database engine must update every index B-tree for every inserted row. For a 211 M-row table with 4 non-clustered indexes that is approximately 844 million extra B-tree page writes interleaved with the inserts — before the migration engine even considers network latency.

Endrias Bridge now automatically disables non-clustered non-unique indexes on the target before the bulk INSERT begins, then rebuilds them in a single pass after all rows are loaded. A single offline rebuild of a fully-loaded index is 2–4× faster than maintaining it row-by-row during load.

What is disabled vs kept

Index typeDuring loadWhy
Non-clustered, non-uniqueDISABLED → rebuilt afterPure query indexes — safe to rebuild offline
Clustered (PK)LEFT INTACTTable data lives in the clustered index — disabling it drops all rows
Unique non-clusteredLEFT INTACTEnforces uniqueness during INSERT — must remain active

What you see in the log

[idx] DISABLED ClaimEnvelope.IX_ClaimEnvelope_DateCreated [idx] DISABLED ClaimEnvelope.IX_ClaimEnvelope_ClaimHeaderId [idx] 2 non-clustered index(es) disabled — will rebuild after data copy ... rows copying ... --- Rebuilding 2 index(es) on target --- [idx] REBUILT ClaimEnvelope.IX_ClaimEnvelope_DateCreated [idx] REBUILT ClaimEnvelope.IX_ClaimEnvelope_ClaimHeaderId --- Index rebuild: 2 rebuilt, 0 error(s) ---
⚠️
Do not close the tool while index rebuild is in progress. The rebuild runs in the finally block after copy — it will complete even if copy errored partway through. But killing the process during rebuild leaves indexes in the DISABLED state. If that happens, connect to the target RDS and run ALTER INDEX ALL ON [tableName] REBUILD manually.

Manually rebuild a disabled index on RDS

If the tool was killed mid-rebuild, run this on the target RDS to find and fix disabled indexes:

-- Find all disabled indexes:
SELECT OBJECT_NAME(i.object_id) AS table_name, i.name AS index_name
FROM sys.indexes i
WHERE i.is_disabled = 1
ORDER BY 1, 2;

-- Rebuild a specific index:
ALTER INDEX [IX_ClaimEnvelope_DateCreated] ON [dbo].[ClaimEnvelope] REBUILD;

-- Or rebuild all indexes on a table at once:
ALTER INDEX ALL ON [dbo].[ClaimEnvelope] REBUILD;

BCP Bulk Copy — Maximum Throughput for SQL Server Migrations

For SQL Server → SQL Server migrations (including Azure SQL → AWS RDS), Endrias Bridge automatically uses bcp.exe when it is available on PATH. BCP eliminates the Python row-by-row overhead by writing a binary bulk stream directly to disk and then loading it via BULK INSERT — the same internal path SQL Server itself uses for bulk operations.

Throughput comparison

MethodTypical speed100 GB estimate
Python SQLAlchemy INSERT (old default)1–5 GB/hr20–100 hours
fast_executemany batch INSERT3–8 GB/hr12–33 hours
BCP OUT + BULK INSERT (current default)10–30 GB/hr3–10 hours

Checking if bcp.exe is installed

:: Check if bcp is on PATH
bcp /?

:: If the command is not found, install it:
winget install Microsoft.SQLServerCommandLineUtils

What happens if bcp.exe is not found

The tool automatically falls back to PK-range checkpoint copy and logs:

TableName: [bcp] bcp.exe not found on PATH — falling back to PK-range copy

The migration continues normally — no data is lost or skipped.

What you see in the log when BCP runs

ClaimEnvelope: [bcp] exporting ClaimEnvelope from sql-target.database.windows.net... ClaimEnvelope: [bcp] importing ClaimEnvelope into target-rds.us-east-1.rds.amazonaws.com (14,203.7 MB)... ClaimEnvelope: 57,432,199 rows done (BCP)

Troubleshooting bcp errors

SymptomCauseFix
bcp exits with rc=1, "Login failed" SQL Server authentication credentials in the connection URL are wrong for bcp Verify username/password in the tool's connection profile; ensure SQL Auth (not Windows Auth) is used on the remote server
bcp exits with rc=1, "Cannot open BCP host data-file" Temp file path is not writable or temp dir is full Free space in the Windows temp directory (%TEMP%); ensure at least 2× table size free
BULK INSERT fails with "Cannot bulk load because the file could not be opened" Target SQL Server cannot read the temp file path (common on RDS over network) This is expected for remote RDS targets — the tool falls back to PK-range copy automatically
bcp shows 0 rows exported Source table is empty at export time Normal behaviour — BCP path sets checkpoint to MAX(pk) = NULL and returns 0 rows copied

🔴 CDC Stream Mode — Near-Real-Time Continuous Replication

The CDC Stream sync mode runs a continuous LSN tail loop that replicates changes from a SQL Server source to the target in near-real-time (< 1 second latency). Use it for cut-over windows where you want to keep the target in sync while user traffic continues on the source.

When to use CDC Stream vs standard CDC

ScenarioRecommended mode
Initial full copy of 10 GB – 1 TBFull copy (with BCP + index disable)
Periodic delta sync (hourly / daily jobs)CDC (single-pass)
Cut-over window — keep target live while traffic runs on sourceCDC Stream
Real-time shadow copy for read-scaleCDC Stream

How to start

  1. Enable CDC on source: EXEC sys.sp_cdc_enable_db;
  2. Enable CDC on each table:
    EXEC sys.sp_cdc_enable_table
        @source_schema = N'dbo',
        @source_name   = N'YourTable',
        @role_name     = NULL;
  3. In Migration tab: select CDC Stream (real-time, SQL Server)
  4. Click Start Migration
  5. Monitor the log — cycles are logged as changes arrive
  6. When ready to cut over: click Stop Stream to drain and close

What you see in the log

--- CDC Stream started: 5 table(s) — click "Stop Stream" to end --- [stream] cycle 1: +247 change(s) (total 247) LSN=0x000000001A4F... elapsed 00:00:01 [stream] cycle 2: +18 change(s) (total 265) LSN=0x000000001A50... elapsed 00:00:02 [CDC Stream] Stop requested — draining current cycle... --- CDC Stream stopped: 265 change(s) in 00:01:43 (2.6 changes/s) ---

Troubleshooting

SymptomCauseFix
Tables are skipped with "no CDC capture instance" CDC is not enabled on that table Run EXEC sys.sp_cdc_enable_table for each skipped table, then restart
No cycles are logged — stream appears idle No changes on source tables Normal — the loop sleeps 250 ms when LSN doesn't advance. Insert a test row to verify.
"CDC is not enabled on the source database" DB-level CDC never enabled Run EXEC sys.sp_cdc_enable_db; on the source once
source read error — retrying... Transient network error to source The loop auto-retries with exponential backoff (5 → 120 s). No action needed.

📥 CDC Initial Load — Step-by-Step

This is the procedure to seed the target with a full copy while CDC is already running, so no changes are lost during the copy window.

Prerequisites

RequirementDetailsHow to verify
SQL Server source On-prem 2012+, Azure SQL Database, Azure SQL MI, or AWS RDS for SQL Server Check Step 5 source type in the wizard
SQL Server Agent running CDC uses Agent jobs to scan the transaction log. Must be running on source. SELECT status_desc FROM sys.dm_server_services WHERE servicename LIKE 'SQL Server Agent%'
db_owner on source Required to run sp_cdc_enable_db and sp_cdc_enable_table SELECT IS_MEMBER('db_owner')
Target database exists Endrias Bridge creates it automatically on first migration run Run Assessment to confirm connectivity
Disk space on source transaction log drive CDC accumulates changes in the log during the copy window. Ensure ≥ 20% free on the log drive. EXEC xp_fixeddrives; on source
Endrias Bridge has network access to both source and target TCP 1433 open in both directions; SSL cert trusted if using encrypted connections Run the Test Connection button on Step 5

Procedure

  1. Enable CDC on the source database
    USE [YourSourceDB];
    EXEC sys.sp_cdc_enable_db;
    SELECT name, is_cdc_enabled FROM sys.databases WHERE database_id = DB_ID();
  2. Enable CDC on each table to track
    EXEC sys.sp_cdc_enable_table
        @source_schema = N'dbo',
        @source_name   = N'YourTable',
        @role_name     = NULL;
  3. Record the current LSN bookmark
    SELECT sys.fn_cdc_get_max_lsn() AS start_lsn;  -- save this value
  4. Run Full migration in Endrias Bridge — Migration tab → sync mode Full → Run Migration
  5. Validate row counts on source and target after copy completes
  6. Re-apply Schema Objects if needed (Schema Objects tab)
  7. Switch to CDC Stream — change sync mode to CDC Stream → Run Migration. The stream catches up accumulated changes from step 3 onward immediately.

Common errors during initial load

ErrorCauseFix
Agent job not found / CDC not accumulating SQL Server Agent is stopped Start Agent: net start "SQL Server Agent (MSSQLSERVER)" (or via SSMS)
Transaction log full during copy CDC is holding the log open while copy runs; log drive is full Free space on log drive; set database to SIMPLE recovery temporarily (only if OK for your RPO); or increase log file autogrowth limit
sp_cdc_enable_db fails with "CDC is not supported on Express edition" SQL Server Express does not support CDC Use Incremental (watermark) sync mode instead; or upgrade to Standard/Enterprise
Tables with no CDC capture skipped by stream sp_cdc_enable_table was not run for those tables Run sp_cdc_enable_table for each skipped table; restart the stream

✂️ CDC Final Cutover — Checklist

Cutover redirects application traffic from source to target. This checklist is designed to keep the downtime window under 60 seconds.

Prerequisites before starting cutover

CheckHow to verify
CDC stream has been running for ≥ 15 minutes after initial copy Confirm in the Migration log: stream cycles show < 10 changes/cycle at steady state
Row counts are within 0–5% between source and target Run the row-count query on both sides (see below)
Schema objects applied on target Schema Objects tab shows 0 errors
Application logins exist on target Users & Logins tab shows all logins present
Maintenance window is approved Stakeholder sign-off; change ticket open

Cutover sequence

#ActionCommand / check
1 Stop application writes to source Put app in maintenance mode OR REVOKE INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [AppUser]
2 Wait for stream to drain (idle cycles) Watch log: two consecutive cycles with no new LSN advance
3 Validate row counts match SELECT t.name, SUM(p.rows) FROM sys.tables t JOIN sys.partitions p ON p.object_id=t.object_id AND p.index_id IN(0,1) GROUP BY t.name — run on both sides
4 Click Stop Stream Migration tab or Replication tab → Stop Stream
5 Redirect application connection string Update app config / DNS alias to point at target server + database
6 Smoke-test application Login, read, write, search — core paths only
7 Monitor for 15 minutes Watch application error log; check target SQL error log
8 Declare success or rollback Success → keep source read-only for 24 h. Rollback → point app back at source, restore write grants

Rollback

If something goes wrong within the first 30 minutes:

  1. Update app connection string back to source
  2. Restore write access on source: GRANT INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [AppUser]
  3. Investigate issue on target; fix; re-drain the stream; re-attempt cutover
After a successful cutover, disable CDC on the source to stop log accumulation: EXEC sys.sp_cdc_disable_db; — run this only after 24 h of stable target operation.

📡 Replication Tab — Guide & Troubleshooting

The Replication tab provides a persistent control panel for ongoing CDC streaming and scheduled watermark sync — separate from the Migration tab so you can monitor replication while the Migration tab is idle.

Prerequisites

RequirementCDC StreamScheduled Sync
Source engine SQL Server only (on-prem / Azure SQL / AWS RDS) Any engine (SQL Server, PostgreSQL, MySQL, etc.)
CDC enabled on source Yes — sp_cdc_enable_db + sp_cdc_enable_table per table Not required
Watermark column on tables Not required (uses LSN) Recommended — UpdatedAt, ModifiedDate, RowVersion. Tables without one are skipped.
Source + target connections configured Both — Step 5 of the wizard must have valid host / db / credentials for source and target
Initial full copy completed Yes — the stream catches delta changes, not a full copy Recommended — first scheduled sync copies all rows for tables with no watermark state

CDC Stream — controls reference

ControlDescription
Poll interval (ms)Frequency of LSN checks. 250 ms = 4 checks/second, near-zero CPU when idle. Minimum 100 ms; maximum 5000 ms.
Start CDC StreamDiscovers CDC-enabled source tables, builds engine pair, starts LSN tail loop in background thread.
Stop StreamSignals graceful stop; current cycle drains before thread exits. State file is flushed.
LSNLast processed Log Sequence Number (hex). Advances with every change cycle.
RateRolling 60-second changes/second average. Turns red above 500/s (high change rate warning).
Total changesCumulative inserts + updates + deletes applied since stream started.
Schema drift alertRed label — a tracked table's column set changed while streaming. Table is suspended until you stop, fix schema, and restart.

Scheduled Sync — controls reference

ControlDescription
Sync every N minutesTimer interval. Set to 1 for near-real-time on non-SQL-Server sources. Set to 60 for hourly batch sync.
Start Scheduled SyncReflects source and target metadata, finds common tables, runs sync_table_smart() per table on the timer.
StopCurrent cycle completes before thread exits.
Next runWall-clock time of next scheduled cycle.
Last runCompletion time of most recent cycle.

Per-table dashboard — column meanings

ColumnMeaning
TableTable name (schema.table if schema-qualified)
Modecdc = SQL Server CDC used; watermark = timestamp column used; no_sync = no watermark and no CDC — table skipped
Rows SyncedCumulative rows applied (INSERTs + UPDATEs) this session
Last SyncTimestamp of last change applied to this table

Troubleshooting

SymptomCauseFix
Start CDC Stream → "ERROR: Configure Source and Target on Step 5 first" Wizard connections not saved Go to Step 5 (DB Migration) in the wizard, fill in all fields, click Save / Next
All tables skipped with "no CDC capture instance" sp_cdc_enable_table never run Run EXEC sys.sp_cdc_enable_table @source_schema='dbo', @source_name='TableName', @role_name=NULL for each table
Stream starts but no cycles are logged (stream appears idle) No writes on source tables Normal. Insert a test row to verify: INSERT INTO dbo.YourTable ... SELECT TOP 1 ... FROM dbo.YourTable
Scheduled sync shows all tables as no_sync No watermark columns detected on any table Add an UpdatedAt DATETIME2 DEFAULT GETUTCDATE() column to key tables, or use CDC Stream instead
Schema drift alert appears on a table A column was added/removed on the source while streaming Stop stream → alter target table schema to match → re-run Re-apply Schema Objects → restart stream
Rate > 500/s warning in log Target IOPS or network is a bottleneck; source producing changes faster than target can apply Increase target IOPS (scale up RDS instance or add Premium storage); reduce source write rate if possible; check for index bloat on target
Replication tab controls greyed out after start Thread started successfully — buttons are disabled to prevent double-start Click Stop to reset; verify log for any ERROR lines

Parallel Streams — Large Table Acceleration

For tables with hundreds of millions or billions of rows, a single copy thread is limited by SQL Server round-trip latency. The parallel-streams feature splits the PK range into N equal segments and copies each in a separate thread, multiplying throughput near-linearly.

How to enable

  1. In the Migration tab, find the Parallel streams per large table spinbox (default: 1)
  2. Set it to 4 for tables 400 M – 1 B rows; 8 for tables over 1 B rows
  3. Leave at 1 to use BCP (faster for smaller tables)
  4. Start migration normally — streams are used automatically for every table

Checkpoint and resume

Each segment has its own checkpoint key (table__seg0, table__seg1, etc.) in migrationbridge_sync_state.json. If the migration is interrupted, already-completed segments are skipped on resume — only the interrupted segment restarts from its last committed batch.

Troubleshooting

SymptomCauseFix
Parallel streams not starting — falls back to single stream Table PK range is smaller than chunk_size × 2 Normal — small tables use single stream automatically
Some segments never start ThreadPoolExecutor busy; slots queue All segments will run; concurrency is capped at min(N, available slots)
Deadlock errors in log Multiple segments hitting same index pages Reduce streams to 2–4; consider WITH (NOLOCK) if allowed for reads
Memory pressure on target N streams × chunk_size rows in memory simultaneously Reduce chunk_size in migrationbridge.conf (e.g. 25000) to stay under 1 GB RAM

Type Override Config — Custom Column Type Mappings

The [type_overrides] section in migrationbridge.conf lets you override how specific source types are mapped to target types without touching Python code. Overrides apply before built-in mapping rules — they take full precedence.

Configuration

# migrationbridge.conf [type_overrides] geography = NVARCHAR(4000) geometry = NVARCHAR(4000) uniqueidentifier = VARCHAR(36) hierarchyid = NVARCHAR(300) xml = TEXT money = DECIMAL(19,4)

Supported target type formats

FormatExample
Simple namexml = TEXT
Name with lengthuniqueidentifier = VARCHAR(36)
DECIMAL with precision and scalemoney = DECIMAL(19,4)
NVARCHARgeography = NVARCHAR(4000)

Troubleshooting

SymptomCauseFix
Override not applying after conf edit Module-level cache not cleared Restart the tool — the override cache is populated at first use per process run
"Unknown type in type_overrides" in log Typo in the type name (e.g. NVARCHAR4000 missing parens) Check syntax: must be NVARCHAR(4000) with parentheses

MongoDB Source Connector

Endrias Bridge can migrate MongoDB collections (Atlas or on-premises) to any relational target. Schema is inferred by sampling up to 200 documents; nested objects and arrays are stored as JSON text columns in the relational target.

Prerequisites

pip install pymongo

Connection URI formats

# Atlas (cloud) mongodb+srv://username:password@cluster0.abc12.mongodb.net/myDatabase?retryWrites=true # On-premises replica set mongodb://username:password@host1:27017,host2:27017/myDatabase?replicaSet=rs0 # Local (dev) mongodb://localhost:27017/myDatabase

Troubleshooting

SymptomCauseFix
ImportError: No module named 'pymongo' pymongo not installed Run pip install pymongo
ServerSelectionTimeoutError Cannot reach MongoDB host Check firewall, VPN, and that MongoDB is running; verify URI
Nested fields not appearing in target Nested dict/list → stored as JSON text in one column Expected behavior — use a JSON function on the target to extract sub-fields
ObjectId columns appearing as strings ObjectId serialized to 24-char hex string Expected — ObjectId has no direct SQL equivalent; VARCHAR(24) is used

Azure Cosmos DB (MongoDB API) Connector

Endrias Bridge migrates relational tables to Azure Cosmos DB using the Cosmos DB for MongoDB API compatibility endpoint. Each source table becomes a collection; rows become JSON documents. SQL Server columns typed as hierarchyid or geography are automatically skipped (logged per table) — all other columns migrate cleanly.

Driver: pymongo — the same driver used for MongoDB. Do not install azure-cosmos (Core/SQL API SDK); it is not used by this tool.

Prerequisites

pip install pymongo

Connection — Cosmos DB MongoDB API endpoint

# Format shown in the Azure Portal → Cosmos DB account → Connection String mongodb://<account>:<primary-key>@<account>.mongo.cosmos.azure.com:10255/<db>?ssl=true&replicaSet=globaldb # In the tool: select target engine = "Cosmos DB" # Host: <account>.mongo.cosmos.azure.com # Port: 10255 (auto-filled) # Username: your Cosmos DB account name # Password: primary or secondary key from the Azure Portal # Database: the Cosmos DB database name to write into

Validated migration (July 2026)

Source tableDocuments writtenNotes
HumanResources.Employee290OrganizationNode (hierarchyid) skipped — all other columns migrated
Person.Address19,614SpatialLocation (geography) skipped — all other columns migrated
Production.Product504Clean migration
Sales.Customer19,820Clean migration
Person.Person19,972Clean migration

Troubleshooting

SymptomCauseFix
ImportError: No module named 'pymongo' pymongo not installed Run pip install pymongo
ServerSelectionTimeoutError on port 10255 MongoDB API not enabled on the Cosmos DB account, or wrong port Azure Portal → Cosmos DB account → verify API = "Azure Cosmos DB for MongoDB". Port must be 10255.
Authentication failed / 401 Wrong account name (username) or key (password) Copy the Primary Connection String from Azure Portal → Cosmos DB → Connection String tab
SSL handshake error pymongo 4.x requires TLS; Cosmos DB enforces it The tool passes tlsAllowInvalidCertificates=True automatically — no action needed
hierarchyid / geography columns missing from documents ODBC type -151 — SQLAlchemy reflects these as NullType; tool skips them Expected behavior (Change 105). The skip is logged per table. Other columns are unaffected.
azure-cosmos SDK errors Wrong package installed This tool uses the MongoDB wire protocol — uninstall azure-cosmos, install pymongo

DynamoDB Source Connector

Endrias Bridge can scan Amazon DynamoDB tables and migrate data to any relational target. Uses the AWS SDK (boto3) with paginated Scan. Decimal, Set, Map, and List types are mapped to standard SQL types automatically.

Prerequisites

pip install boto3

Connecting

Supply AWS region + credentials. The connector accepts:

Troubleshooting

SymptomCauseFix
ImportError: No module named 'boto3' boto3 not installed Run pip install boto3
NoCredentialsError No AWS credentials configured Set env vars AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or configure an IAM role
Sparse columns showing as NULL for most rows DynamoDB allows per-item optional attributes Expected — columns inferred from the sample will be NULL where absent in items
ProvisionedThroughputExceededException during scan Scan consuming too many read units Reduce parallel streams or add a time.sleep between pages (advanced: add retry logic)

Web Dashboard — Setup and Use

The built-in web dashboard provides a live browser view of migration progress at http://localhost:8765. Designed for headless VM and Docker deployments where a Tkinter GUI is not available. All HTML/CSS/JS is embedded — works with no internet.

Prerequisites

pip install fastapi "uvicorn[standard]"

Starting the dashboard

In headless mode (--headless CLI flag or Docker), the dashboard starts automatically. In GUI mode, the web server is started by db_webserver.start_server(state) when a migration run begins — open http://localhost:8765 in any browser.

Dashboard features

REST API (for CI/CD)

# Poll until done curl http://localhost:8765/api/status {"status":"running","percent":42,"rows_done":1843000000,...} # Get last 50 log lines curl "http://localhost:8765/api/log?n=50"

Troubleshooting

SymptomCauseFix
ImportError: No module named 'fastapi' fastapi/uvicorn not installed Run pip install fastapi "uvicorn[standard]"
Port 8765 already in use Another process using the port Pass port=8766 to start_server() or kill the conflicting process
Dashboard not loading from another machine Server binds to 0.0.0.0 but Windows firewall blocks port 8765 Add an inbound firewall rule for TCP 8765 on the migration host

Docker Deployment

Endrias Bridge ships with a Dockerfile and docker-compose.yml for containerized headless deployment. The image includes the Microsoft ODBC 18 driver for SQL Server connectivity. The web dashboard is available on port 8765.

Quick start

# 1. Edit config nano migrationbridge.conf # 2. Build docker build -t endrias-bridge . # 3. Run docker run -p 8765:8765 \ -v $(pwd)/migrationbridge.conf:/app/migrationbridge.conf:ro \ -v $(pwd)/migration_state:/app/state \ endrias-bridge # 4. Open dashboard open http://localhost:8765 # Or with Compose docker compose up migration

Volume mounts

Host pathContainer pathPurpose
./migrationbridge.conf/app/migrationbridge.confConfig (read-only)
./migration_state//app/state/Checkpoint JSON — survives container restart

Troubleshooting

SymptomCauseFix
ODBC driver not found in container Image build failed to install msodbcsql18 Check build output for apt errors; ensure internet access during build (needs Microsoft apt repo)
Config changes not picked up Container uses cached image Rebuild: docker compose build --no-cache migration
Migration restarts from zero after container restart State volume not mounted Ensure ./migration_state:/app/state volume is in docker-compose.yml
bcp.exe not available — falling back to parallel streams bcp.exe is Windows-only; not available on Linux image Expected — set Parallel streams to 4–8 for best throughput in Docker

Constraints & Indexes That Don't Fully Migrate — What To Do

Three index types are silently skipped (✗) and two are migrated with behavioral differences (~). None of these cause data loss, but all can cause silent performance regressions or broken queries after cutover if not addressed. The pre-migration inventory log ([inventory] lines) lists every affected table before copy begins — use it as your post-migration checklist.

~ Partial — Migrates with caveats

CLUSTERED Index → B-Tree

What SQL Server does: The clustered index physically sorts and stores table rows on disk in PK order. Range scans (WHERE ClaimId BETWEEN 1 AND 1000000) are extremely fast because the database reads contiguous pages.

What the migration does: A standard B-Tree index is created on the target on the same column(s). The index exists and queries will use it — but rows are stored in heap order (insertion order), not PK order.

Performance implication by target engine:

TargetImpactFix
PostgreSQL / Aurora PG Range scans on PK may be slower — heap pages scattered, more random I/O Run CLUSTER <table> USING <pk_index>; post-migration. One-time cost; significant read gain for billion-row tables. Schedule during low-traffic window — CLUSTER holds an exclusive lock.
MySQL / Aurora MySQL (InnoDB) No impact — InnoDB always clusters on the PK automatically Nothing needed.
SQL Server / Azure SQL target No impact — clustered index DDL is emitted unchanged Nothing needed.
For a 1 B-row table on PostgreSQL, an unclustered heap can add 30–50% extra I/O for range-scan-heavy workloads (reporting, CDC history queries). Run CLUSTER before going live if the table is queried by PK range.

Filtered Index → Partial Index

What SQL Server does: A filtered index only indexes rows matching a WHERE clause — e.g. CREATE INDEX IX_Active ON Claims(ClaimId) WHERE IsActive = 1. This makes the index tiny and fast for the common case.

What the migration does: The tool attempts to translate the WHERE clause to a PostgreSQL partial index. Simple equality filters (col = 1, col IS NOT NULL) translate cleanly. Filters that use SQL Server–specific functions (ISNULL, CONVERT, CASE, GETDATE()) will fail DDL on the target with a syntax error — the index is silently skipped and a warning is logged.

Performance implication: A skipped partial index means the query planner will either use the full unfiltered B-Tree index (more I/O) or do a full table scan (severe regression on large tables). This is a silent regression — no error at query time.

TargetImpactFix
PostgreSQL / Aurora PG Simple filters migrate fine. Complex filters skipped — check log for [idx] WARN: filtered index … could not be created Rewrite the WHERE clause in PostgreSQL syntax and apply manually: CREATE INDEX … WHERE col IS NOT NULL;
MySQL / Aurora MySQL MySQL does not support partial indexes — all filtered indexes are silently dropped Evaluate whether a full index on the column is acceptable, or use a covering index with the filter column included. For very selective filters consider a generated column + index.
SQL Server / Azure SQL target No impact — filtered index DDL emitted unchanged Nothing needed.

✗ Not Migrated — Requires manual post-migration work

COLUMNSTORE Index

SQL Server clustered and non-clustered columnstore indexes store column data in compressed column groups enabling batch-mode execution — typically 10–100× faster than row-mode for aggregation-heavy queries (SUM, COUNT, GROUP BY over millions of rows). They are entirely skipped during migration with no error.

How to find them before migration:

SELECT t.name AS table_name, i.name AS index_name, CASE i.type WHEN 5 THEN 'CLUSTERED COLUMNSTORE' WHEN 6 THEN 'NONCLUSTERED COLUMNSTORE' END AS index_type FROM sys.indexes i JOIN sys.tables t ON i.object_id = t.object_id WHERE i.type IN (5, 6);
TargetFix
SQL Server / Azure SQL target Recreate with the same DDL: CREATE CLUSTERED COLUMNSTORE INDEX … ON <table>; or CREATE NONCLUSTERED COLUMNSTORE INDEX … ON <table> (<cols>);. Azure SQL Database and AWS RDS for SQL Server both support columnstore.
PostgreSQL / Aurora PG No native columnstore. Options: (a) accept row-mode performance for analytical queries, (b) use a materialized view with pre-aggregation for the most common reporting queries, (c) evaluate columnar storage extensions (cstore_fdw, Hydra, or Redshift for pure analytics).
MySQL / Aurora MySQL No native columnstore. Consider HeatWave (Oracle MySQL HeatWave Analytics) or move reporting workloads to a dedicated analytics replica.

Full-Text Index

SQL Server full-text indexes support linguistic queries via CONTAINS(), FREETEXT(), and CONTAINSTABLE(). They are stored in full-text catalogs outside the normal index structure and have no SQLAlchemy metadata representation — they are silently skipped.

How to find them before migration:

SELECT OBJECT_NAME(fi.object_id) AS table_name, fc.name AS catalog_name, COL_NAME(fic.object_id, fic.column_id) AS column_name FROM sys.fulltext_indexes fi JOIN sys.fulltext_catalogs fc ON fi.fulltext_catalog_id = fc.fulltext_catalog_id JOIN sys.fulltext_index_columns fic ON fi.object_id = fic.object_id;

Impact: Any application query using CONTAINS() or FREETEXT() will return an error on the target ("No full-text catalog" / "column is not full-text indexed").

TargetFix
SQL Server / Azure SQL target CREATE FULLTEXT CATALOG [MyCatalog]; then CREATE FULLTEXT INDEX ON <table>(<col>) KEY INDEX <pk> ON [MyCatalog];
PostgreSQL / Aurora PG Add a tsvector generated column + GIN index: ALTER TABLE t ADD COLUMN fts tsvector GENERATED ALWAYS AS (to_tsvector('english', col)) STORED; then CREATE INDEX ON t USING GIN(fts);. Rewrite queries to use @@ operator.
MySQL / Aurora MySQL (InnoDB 5.6+) ALTER TABLE t ADD FULLTEXT INDEX ft_idx(col);. Queries use MATCH(col) AGAINST('term' IN BOOLEAN MODE) instead of CONTAINS.

Spatial Index

SQL Server spatial indexes accelerate geographic queries on geography and geometry columns (STIntersects, STDistance, bounding-box filters). The column data is migrated as WKT text (Change 17), but the index is not.

How to find them before migration:

SELECT OBJECT_NAME(object_id) AS table_name, name AS index_name FROM sys.spatial_indexes;

Impact: Spatial queries will do a full table scan — negligible for small lookup tables, severe for tables with millions of geometry rows (e.g. a claims geography table with per-claim location).

Important: If you used [type_overrides] geography = NVARCHAR(4000) in migrationbridge.conf, the column arrives on the target as a plain text column. You cannot add a spatial index to a NVARCHAR column. To support spatial queries on the target you must either: (a) not use the type override and keep the column as geography/geometry, or (b) add a computed geometry column from the WKT text post-migration.
TargetFix
SQL Server / Azure SQL target Column type is preserved as geography/geometry. Recreate: CREATE SPATIAL INDEX … ON <table>(<col>) USING GEOGRAPHY_GRID …;
PostgreSQL / Aurora PG Install PostGIS: CREATE EXTENSION IF NOT EXISTS postgis;. Change column type: ALTER TABLE t ALTER COLUMN col TYPE geometry USING ST_GeomFromText(col);. Then: CREATE INDEX ON t USING GIST(col);. Spatial query functions become ST_Intersects(), ST_Distance() etc.
MySQL / Aurora MySQL Change column to GEOMETRY NOT NULL then ALTER TABLE t ADD SPATIAL INDEX(col); (InnoDB 5.7.5+). Column must be NOT NULL for a spatial index.

Post-migration checklist

  1. Search the migration log for [inventory] COLUMNSTORE, [inventory] FULLTEXT, [inventory] SPATIAL — these lines list every affected table
  2. Search the migration log for [idx] WARN: filtered index — lists every partial index that failed to create
  3. For PostgreSQL targets with large tables: run CLUSTER on high-traffic PK-range scan tables
  4. Recreate columnstore / full-text / spatial indexes using the engine-appropriate DDL above
  5. Run your application's most critical queries with EXPLAIN / EXPLAIN ANALYZE and confirm index usage before cutover

🐢 Migration Is Too Slow — Diagnosis & Runbook

If a migration is taking far longer than expected, work through the steps below in order. Each step has a quick check and a specific fix. For the acme_audit database (52 GB, 17 hours observed) the root causes were steps 1 and 2 combined.

Expected throughput baseline

SetupExpected throughput52 GB estimate
Laptop over VPN, 5,000-row batches (old default)200–500 MB/hr10–26 hrs
Laptop over VPN, 100,000-row batches1–2 GB/hr2–4 hrs
Azure VM (same region as source), 100,000-row batches5–15 GB/hr20–60 min
Azure VM + AWS Direct Connect / VPC peering, 100,000-row batches15–30 GB/hr6–20 min

Step 1 — Check and fix batch size

The batch size controls how many rows are fetched and inserted per round-trip. The default is 100,000 rows (as of v1.3.1 — previously 5,000 before the Config auto-load fix). On a 13.8 million-row table (e.g. auditdata.Claims) that is 138 round-trips. Every round-trip adds network latency overhead, so larger batches are significantly faster over WAN.

Check: Open the migration log and look for:

Copying in batches of 100000 rows

If you still see 5000, the app is running an older build — update to the latest installer. The value is set in migrationbridge.conf:

[DEFAULT]
# ─── DB Migration tuning ──────────────────────────────────────────────────────
# Confirmed setting for Azure SQL → AWS RDS (narrow tables, < 2 KB/row)
db_migration_chunk_size = 100000
⚠️
Restart the MigrationBridge GUI after saving the file — the value is read at startup. Verify the next run's log shows Copying in batches of 100000 rows.

Step 2 — Move the migration machine closer to the data

When MigrationBridge runs on a laptop or on-prem workstation, every batch travels:

  1. Azure SQL → internet/VPN → your machine (source read)
  2. Your machine → internet/VPN → AWS RDS (target write)

At typical VPN latency (20–80 ms round-trip), 2,760 round-trips × 80 ms = 3.7 minutes of pure latency overhead for a single table at 5k batches — before counting actual data transfer time. Even at 100k batches that's still 220 round-trips = 18 seconds of dead time per table.

Ideal topology (cut latency from 80 ms to <2 ms):

HopCurrent (laptop/VPN)Optimal
Source read (Azure SQL) VPN → on-prem → Azure (20–80 ms) Azure VM in same region as Azure SQL (<1 ms)
Target write (AWS RDS) Azure VM → public internet → AWS (15–50 ms) Azure VM → ExpressRoute / VPN to AWS VPC (<5 ms)

Quickest option without infra changes: Use an Azure VM in the same region as your Azure SQL source. Even without Direct Connect to AWS, intra-Azure read latency drops to near-zero and the internet hop to RDS is a single clean path.

Recommended EC2 alternative: Spin up a Windows EC2 instance in the same AWS region as the RDS target. Connect it to Azure SQL over the public endpoint (SSL, port 1433 is already open). This eliminates the write-side latency entirely since the machine and the RDS instance are in the same VPC.

Step 3 — Check RDS instance size and IOPS

Bulk INSERT pressure all lands on the target. If the RDS instance is undersized for the migration window, storage IO becomes the bottleneck — independent of batch size or network.

Check in CloudWatch during a live migration run:

Fix (migration window only — scale back after):

BottleneckFix
Write latency > 20 msSwitch storage to gp3 and provision 6,000–12,000 IOPS (free to provision up to 16,000 on gp3)
CPU > 90%Scale instance class up one tier (e.g. db.m5.large → db.m5.xlarge). Takes ~5 min for Multi-AZ.
FreeStorageSpace dropping fastPre-grow allocated storage, or switch target databases to SIMPLE recovery model during the migration window to minimize log growth

Step 4 — Check for the decimal precision retry fallback

If the decimal precision bug (now fixed in the current build) was active, every affected batch silently retried row-by-row. A 100k-row batch retried one row at a time = 100,000 INSERT round-trips. This would show up as tables taking 10–100× longer than others.

Check: Scan the migration log for:

WARNING: batch truncation / precision error, retrying 100000 rows individually

If you see this with large row counts, rebuild MigrationBridge.exe with the latest source and re-run. The fix retries only the failing batch, not every subsequent batch.

Step 5 — Validate throughput after changes

After applying fixes, compare throughput in the migration log summary line:

--- Full copy: 13,883,883 rows | 00:04:22 | 52,942 rows/sec ---

At 100k batches over a low-latency path, expect 30,000–80,000 rows/sec for narrow tables. For a typical audit database (avg ~350 bytes/row):

TableRowsExpected duration (40k rows/sec)
auditdata.Claims13,883,883~5.8 min
analyticsdata.AggregateByEditFact13,972,964~5.8 min
analyticsdata.AggregateLineFact26,923,039~11.2 min
analyticsdata.AggregateHeaderDxCodeFact15,536,105~6.5 min
All other tables (<3M rows each)~5 M combined<3 min combined

Total acme_audit at 40k rows/sec: ~35–45 minutes vs the original 17 hours.

💡
Next-run checklist:
  1. Confirm migrationbridge.conf has db_migration_chunk_size = 100000
  2. Run from an Azure VM or EC2 instance — not a laptop over VPN
  3. Switch RDS storage to gp3 and set IOPS to 6,000 for the migration window
  4. Verify the run is a Full migration (schema + data), not Resume
  5. Monitor CloudWatch WriteLatency — target <5 ms
  6. Post-migration: scale RDS storage IOPS back to baseline and sign off the comparison report

Batch Size Tuning — What To Do and NOT To Do

The db_migration_chunk_size key in migrationbridge.conf controls how many rows are fetched from the source and inserted into the target in a single round-trip. Getting this right is the single biggest lever for migration speed and stability.

Step 1 — Measure actual row width before changing the value

Run this on the source database first. The result tells you whether it is safe to increase the batch size:

-- Estimated max row width (defined column sizes)
SELECT TOP 20
    t.name                                              AS table_name,
    SUM(c.max_length)                                   AS estimated_row_bytes,
    SUM(c.max_length) * 200000 / 1073741824.0           AS est_GB_at_200k
FROM sys.tables  t
JOIN sys.columns c ON c.object_id = t.object_id
GROUP BY t.name
ORDER BY estimated_row_bytes DESC;

-- Actual average row width (real storage)
SELECT TOP 20
    t.name                                                              AS table_name,
    p.rows                                                              AS row_count,
    (SUM(a.total_pages) * 8192.0) / NULLIF(p.rows, 0)                  AS avg_actual_bytes_per_row
FROM sys.tables          t
JOIN sys.indexes         i  ON i.object_id = t.object_id AND i.type <= 1
JOIN sys.partitions      p  ON p.object_id = t.object_id AND p.index_id = i.index_id
JOIN sys.allocation_units a ON a.container_id = p.partition_id
GROUP BY t.name, p.rows
ORDER BY avg_actual_bytes_per_row DESC;
⚠️
Tables with only 1–5 rows will show very high avg_actual_bytes_per_row (e.g. 73,728) — this is SQL Server 8 KB page allocation overhead, not real row data. Ignore rows where row_count < 100. Focus on tables with thousands of rows.

Step 2 — Apply the safe batch size table

Batch Size Max safe actual bytes/row Use when
20,000 – 25,000 Any Wide tables, BLOB / XML / nvarchar(MAX) columns, slow or high-latency network, small RDS instance (db.t3.*)
50,000 < 5,000 bytes/row Narrow-to-medium tables, stable network — proven safe floor for Azure SQL → AWS RDS
100,000 < 2,500 bytes/row Narrow tables, fast network, > db.m5.large RDS — confirmed stable for enterprise workloads
200,000 < 1,000 bytes/row Very narrow tables (< 20 short columns), fast dedicated network, large RDS instance — benchmark/verify first
500,000+ Dev / test only. Never production.

DO — recommended practices

DO NOT — common mistakes

🚫
DO NOT set above 200,000 on tables with nvarchar(MAX), varbinary(MAX), xml, or image columns. These columns can be 10–100 KB per row. A 200k batch on a 50 KB/row table = 10 GB in RAM — will OOM the migration machine or timeout the RDS connection mid-batch.

Endrias Bridge Confirmed Settings (Azure SQL → AWS RDS)

SettingValueNotes
Batch size (tested stable)50,000Safe floor — confirmed no OOM, no timeout
Batch size (current production)100,000Running clean; actual row widths all < 2,000 bytes
Widest table (actual)captured_columns — 1,993 bytes/row~190 MB per batch at 100k — well within safe range
RDS watch itemTransaction log free spaceMonitor during large runs; pre-grow or enable auto-growth

🖥 Where to Increase Resources

When migration throughput is low or you want to safely raise the batch size, resource changes belong on the target (AWS RDS), not the source.

ℹ️
The source database (Azure SQL) is read-only during migration — low IO stress. The migration machine holds the batch in RAM. The target (AWS RDS) takes all the write pressure — that is where bottlenecks appear and where resources should be added.
ResourceWhereWhat to do
RAM (batch buffer) Migration machine (your PC / jump host) Close other apps during migration. Rule of thumb: leave 2× the batch memory free. At 100k rows × 2,000 bytes = ~190 MB per batch; 4 GB free is plenty.
IOPS / Storage throughput AWS RDS Switch from gp2 to gp3 (free baseline 3,000 IOPS) or provision io1/io2 IOPS for the migration window. Revert after migration to save cost.
Instance class AWS RDS Scale up for the migration window, e.g. db.m5.largedb.m5.xlarge. Scale back down afterwards. Takes ~5 min for Multi-AZ instances (brief failover).
max server memory AWS RDS Parameter Group Set to 75–80 % of instance RAM. Leaves headroom for OS and connection buffers. Apply via Parameter Group — no reboot needed for dynamic parameters.
Transaction log space AWS RDS Bulk inserts generate heavy log. Pre-grow the log file or confirm auto-growth is enabled. Monitor FreeStorageSpace in CloudWatch during the run.
Network bandwidth Migration machine ↔ AWS RDS Run the migration machine in the same AWS region as the RDS instance (EC2 or SSM Session Manager on a VPC host). Avoids internet latency and egress charges.
💡
Never increase resources on the source (Azure SQL). Azure SQL Database DTUs / vCores affect read throughput, but the migration reads sequentially in chunks — the source is rarely the bottleneck. Adding DTUs costs money with no measurable speed gain. Focus all resource investment on the target RDS instance and the network path between the migration machine and RDS.

Concurrent Migrations — Resource Planning

Each migration job is an independent Python thread. When multiple jobs target the same RDS instance, they do not directly block each other (locks are scoped per database), but they compete silently for every shared resource on the target.

Contention map — 5 concurrent jobs, same RDS target

Resource1 job alone5 jobs concurrentBlocks?
Transaction log writer Sequential log flushes 5 simultaneous WRITELOG queues on one volume Yes — log writer serializes. Most common bottleneck.
IOPS / storage throughput gp3 baseline: 3,000 IOPS 5× demand = 12,000–15,000 IOPS needed Yes — write stalls, PAGEIOLATCH_EX waits spike.
CPU 1 of 4 cores busy All 4 cores saturated Yes — INSERT plan compilation + cache churn.
Buffer pool (RAM) Up to 12 GB cache ~2.4 GB effective per DB Indirect — PLE drops but stays healthy if > 300.
Row-level / table locks Per-table locks Separate locks per DB — no cross-DB contention No — different databases never block each other.
Network (migration machine) 1 batch in transit 5 batches in transit simultaneously Possible — check NIC / VPC bandwidth.

Recommended RDS spec for 5 concurrent × 55 GB migrations

ResourceCurrent (1 job)Need for 5 concurrent
Instance class4 vCPU / 16 GB (db.m5.xlarge)db.r5.2xlarge — 8 vCPU / 64 GB
IOPS3,000 (gp3 default)12,000 – 16,000 provisioned IOPS
Storage throughput125 MB/s500 MB/s
Storage size55 GB × 5 + 30% log overhead = 360 GB minimum
max server memory12,097 MB (75% of 16 GB)52,000 MB (80% of 64 GB)
Recovery modelFULLSIMPLE on all target DBs during migration window

Three ways to eliminate contention (choose one)

  1. Option A — Stagger job starts (cheapest, no infra change)

    Start each job 10–15 minutes apart. By the time job 5 starts, job 1 is past its largest tables and IO demand is already falling. Peak overlap drops from 5 jobs to 2–3 jobs. No cost. Total wall-clock time increases slightly but every job finishes reliably.

    T+00:00  Start DB1
    T+10:00  Start DB2
    T+20:00  Start DB3
    T+30:00  Start DB4
    T+40:00  Start DB5
  2. Option B — Provision IOPS + SIMPLE recovery (best single-instance approach)

    Raise gp3 IOPS to 9,000–12,000 in the AWS Console (takes effect in minutes, no reboot). Set SIMPLE recovery on all 5 target databases before starting. These two changes together eliminate the two most common bottlenecks (log writer + IOPS).

    -- Set SIMPLE recovery on all target DBs before migration
    ALTER DATABASE [TargetDB1] SET RECOVERY SIMPLE;
    ALTER DATABASE [TargetDB2] SET RECOVERY SIMPLE;
    ALTER DATABASE [TargetDB3] SET RECOVERY SIMPLE;
    ALTER DATABASE [TargetDB4] SET RECOVERY SIMPLE;
    ALTER DATABASE [TargetDB5] SET RECOVERY SIMPLE;
    
    -- After migration completes — restore FULL and seed log chain
    ALTER DATABASE [TargetDB1] SET RECOVERY FULL;
    BACKUP LOG [TargetDB1] TO DISK = 'nul';
    -- repeat for each DB
  3. Option C — Separate RDS instance per database (zero contention, production-grade)

    Each database migrates to its own RDS instance. No shared resources, no contention. Run all 5 simultaneously. Each instance has full 12 GB cache, full IOPS baseline, no lock competition. Spin up the extra instances for the migration window only — consolidate or terminate after.

Pre-flight SQL — run on target before concurrent migration

-- 1. Confirm recovery model on all target DBs
SELECT name, recovery_model_desc, log_reuse_wait_desc
FROM sys.databases
WHERE name IN ('TargetDB1','TargetDB2','TargetDB3','TargetDB4','TargetDB5');

-- 2. Pre-grow log files to avoid autogrowth stalls (set to 10-20% of source DB size)
USE [TargetDB1];
ALTER DATABASE [TargetDB1] MODIFY FILE (NAME = TargetDB1_log, SIZE = 5120MB);

-- 3. Monitor waits during concurrent run — run every 60 seconds
SELECT
    r.session_id,
    r.wait_type,
    r.wait_time_ms,
    r.blocking_session_id,
    DB_NAME(r.database_id)  AS db_name,
    r.status
FROM sys.dm_exec_requests r
WHERE r.wait_type IS NOT NULL
  AND r.wait_type NOT IN ('SLEEP_TASK','WAITFOR','BROKER_TO_FLUSH')
ORDER BY r.wait_time_ms DESC;
Wait type seenBottleneckFix
WRITELOGLog writer — IOPSSIMPLE recovery + provision IOPS
PAGEIOLATCH_EXIOPS ceiling hitProvision 9,000–12,000 IOPS in AWS Console
RESOURCE_SEMAPHOREMemory pressureReduce batch size to 50,000 or upgrade instance
CXPACKETParallelism — normal under loadNo action needed
LCK_M_*Lock contentionShould not appear — different DBs never block each other

Decision matrix

ScenarioRecommended approach
5 DBs, test/dev, cost is priorityStagger + SIMPLE recovery + keep 100k batch
5 DBs, production, tight migration windowdb.r5.2xlarge + 12,000 IOPS + SIMPLE recovery
5 DBs, zero contention requiredSeparate RDS instance per DB (best option)
Migration machine is the bottleneckRun one EC2 jump host per DB in same VPC as RDS

🧠 Reading Memory Health Metrics

Three queries give a complete picture of RDS memory health. Run these before and during migration to confirm no memory pressure.

Query 1 — Page Life Expectancy (PLE)

SELECT
    object_name,
    counter_name,
    cntr_value                              AS current_ple_seconds,
    cntr_value / 60                         AS minutes,
    CASE
        WHEN cntr_value >= 5000  THEN 'EXCELLENT'
        WHEN cntr_value >= 1000  THEN 'HEALTHY'
        WHEN cntr_value >= 300   THEN 'WATCH'
        ELSE                          'CRITICAL — reduce batch size immediately'
    END                                     AS status
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
  AND object_name LIKE '%Buffer Manager%';
PLE valueMeaningAction
> 5,000Excellent — entire working set in RAMNone
1,000 – 5,000Healthy — good buffer utilizationNone
300 – 1,000Under load — monitor closelyWatch trend
< 300Critical — severe page evictionReduce batch size or upgrade instance RAM
💡
Confirmed baseline: PLE = 12,000 on 4 vCPU / 16 GB RDS instance with batch size 100,000. Working dataset fits entirely in the 12 GB buffer pool. During 5 concurrent migrations, expect PLE to drop to 1,000–3,000 — still healthy.

Query 2 — Process Memory (committed vs physical)

SELECT
    physical_memory_in_use_kb / 1024        AS committed_mb,
    page_fault_count,
    memory_utilization_percentage
FROM sys.dm_os_process_memory;
MetricExample observed valueWhat it means
committed_mb 12,220 MB SQL Server committed memory. Slightly above target_memory_mb is normal — expected behaviour.
memory_utilization_percentage 100% This is GOOD, not bad. It means 100% of SQL Server's committed memory is resident in physical RAM — nothing has been paged out to disk. Only worry if this drops below 80% (OS is stealing pages from SQL Server).
page_fault_count 73,605,612 (cumulative since Feb 2026) Cumulative counter — divide by uptime seconds to get rate. 73M faults ÷ 120 days ÷ 86,400 sec = ~7 faults/sec. Negligible. Most are soft faults (page already in RAM). Hard faults near zero when PLE is high.
⚠️
Common misreading: memory_utilization_percentage = 100 does NOT mean "out of memory." It means SQL Server's entire buffer pool is backed by physical RAM with nothing paged to disk — the ideal state. The only danger signal is when this value falls significantly below 100%.

Query 3 — Server-level memory summary

SELECT
    cpu_count,
    hyperthread_ratio,
    physical_memory_mb,
    committed_memory_mb,
    target_memory_mb,
    max_workers_count,
    scheduler_count,
    sqlserver_start_time
FROM sys.dm_os_sys_info;
ColumnExample valueWhat to check
physical_memory_mb16,111 MBTotal instance RAM — matches RDS instance class
target_memory_mb12,097 MBSQL Server memory ceiling — should be 75–80% of physical
committed_memory_mb12,097 MBAt ceiling is normal and expected
cpu_count4vCPUs available — upgrade instance class if CPU waits appear during concurrent runs
max_workers_count512Default for 4-CPU. Adequate for migration workloads.
scheduler_count4One scheduler per vCPU — correct

Batch size memory safety at current instance specs

Batch sizeMemory per batch (widest table: 1,993 bytes/row)% of 12 GB SQL Server budgetSafe?
50,000~95 MB0.8%✅ Yes
100,000~190 MB1.6%✅ Yes — confirmed production
200,000~380 MB3.1%✅ Yes — safe for this row width
500,000~950 MB7.9%🟡 Borderline — monitor PLE

💾 IO Stall Check Before Concurrent Run

Run this query on the target RDS instance before starting concurrent migrations. It reveals whether IOPS is already being hit by existing workloads. If avg_write_ms > 20, provision more IOPS before proceeding.

SELECT
    DB_NAME(vfs.database_id)                        AS db_name,
    mf.physical_name,
    vfs.io_stall_read_ms,
    vfs.num_of_reads,
    CASE WHEN vfs.num_of_reads = 0 THEN 0
         ELSE vfs.io_stall_read_ms / vfs.num_of_reads
    END                                             AS avg_read_ms,
    vfs.io_stall_write_ms,
    vfs.num_of_writes,
    CASE WHEN vfs.num_of_writes = 0 THEN 0
         ELSE vfs.io_stall_write_ms / vfs.num_of_writes
    END                                             AS avg_write_ms
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf
    ON vfs.database_id = mf.database_id
   AND vfs.file_id     = mf.file_id
ORDER BY vfs.io_stall_write_ms DESC;
avg_write_msMeaningAction
< 5 msExcellent — IOPS not a bottleneckProceed with concurrent migration
5 – 20 msModerate — some IO pressureConsider provisioning IOPS before adding more concurrent jobs
20 – 50 msHigh — IOPS ceiling being hitProvision 9,000–12,000 IOPS in AWS Console before proceeding
> 50 msCritical — severe IO stallStop and provision IOPS immediately. Do not run concurrent migrations.

How to provision IOPS on AWS RDS (gp3)

  1. Open RDS Console

    AWS Console → RDS → Databases → select your instance → Modify

  2. Change storage settings

    Storage type: gp3 (if not already). Provisioned IOPS: set to 9,000 – 12,000 for 5 concurrent migrations. Throughput: set to 500 MB/s.

  3. Apply immediately

    Select "Apply immediately" — takes effect in 2–5 minutes with no downtime for gp3 IOPS changes. No reboot required.

  4. Revert after migration

    Drop IOPS back to 3,000 and throughput to 125 MB/s after all migrations complete to avoid unnecessary storage cost.

💡
Log space monitoring during concurrent run. Run this every few minutes on the target to ensure transaction log space does not fill up:
SELECT name,
       log_size_mb  = size * 8.0 / 1024,
       log_used_mb  = fileproperty(name, 'SpaceUsed') * 8.0 / 1024,
       log_used_pct = 100 * fileproperty(name, 'SpaceUsed') / size
FROM sys.databases
WHERE name = DB_NAME();
If log_used_pct climbs above 80%, run CHECKPOINT (SIMPLE recovery) or BACKUP LOG [db] TO DISK = 'nul' (FULL recovery) to free log space.

🔀 Full Migration vs Resume — When to Use Each

MigrationBridge has two migration entry points. Choosing the wrong one causes either unnecessary re-work (Full when Resume would do) or silent INSERT failures (Resume when the schema is missing). Here is the decision rule:

Situation on targetUseWhy
Tables not created (entirely missing from target) Full migration Resume skips CREATE TABLE entirely. It will try to INSERT into tables that don't exist and fail for every table.
Tables exist, data copy was interrupted mid-run Resume Resume reads each table's PK checkpoint from migrationbridge_sync_state.json, or auto-detects the MAX PK from the target if no checkpoint was saved. Continues from that exact PK — no rows deleted, no progress lost. Only tables with no checkpoint AND no detectable MAX PK are cleared and restarted.
Tables exist, all row counts already match Resume Resume will detect every table as "already complete" and exit cleanly with nothing to do — equivalent to a dry-run validation.
Migration completed but schema objects (procs/temporal) not applied Schema Objects tab → Re-apply Schema Objects Neither Full nor Resume is needed — Re-apply runs only _copy_schema_objects without touching table data.

How Full migration handles pre-existing targets

Full migration calls meta.drop_all(tgt_eng, tables=ordered, checkfirst=True) before create_all. The checkfirst=True flag means:

💡
No manual DROP needed. Running Full migration on a target that already has some tables is safe — the tool handles the cleanup automatically. You do not need to pre-drop anything before starting a Full migration.

How Resume detects progress — fast catalog count

Resume uses sys.dm_db_partition_stats (instant catalog lookup) to count rows on both source and target — not COUNT_BIG(*). This means the pre-flight check for a database with 15+ billion-row tables completes in seconds rather than 20+ minutes. If the catalog returns no result, it falls back to COUNT_BIG(*) only for that table.

How Resume shows accurate progress percentage and ETA

The row counts collected during the SKIP pre-flight are summed into skipped_rows and passed to the copy engine as already_done. Both grand_total (the denominator) and _rows_done (the numerator) are seeded with this value before copying starts. The result: if 61 of 75 tables are already complete and represent 80% of the database rows, the progress bar starts at ~80% and climbs to 100% as the remaining 14 tables copy. The ETA is also accurate — it is calculated against actual remaining rows, not a 14-table subset.

How Resume continues from a checkpoint

For every table where target rows < source rows, Resume checks migrationbridge_sync_state.json for a saved PK checkpoint. If found, it resumes the copy with WHERE pk > last_pk — no delete, no restart. If no checkpoint is saved (e.g. first time using the new build), Resume automatically queries SELECT MAX(pk_col) from the target table, saves that value as the checkpoint, and continues from there. Only tables where neither a checkpoint nor a MAX PK can be determined are cleared and restarted from row 0.

How Resume handles missing tables

Resume queries sys.dm_db_partition_stats for each table. If the table doesn't exist, the query returns no rows and _row_count() returns None. A None target count is treated as "not on target yet — will migrate" and the table is queued for migration. However, Resume never runs schema creation — tables must already exist on the target. If the schema is missing, use Full migration instead.

Do not use Resume when the data schema is missing. Every table will appear as "not on target yet" and be queued, then fail with Invalid object name 'schema.table' on the first INSERT. Use Full migration instead.

🏔️ Migrating Billion-Row / TB-Scale Tables

Endrias Bridge has been validated against databases with individual tables exceeding 1 billion rows and total database sizes of 1.41 TB (production environment, June 2026). The following features work together to handle this scale.

PK-range checkpoint seeding

Every table with a single INTEGER or BIGINT primary key uses WHERE pk > last_committed_pk chunking instead of OFFSET. The last PK is saved after every committed batch. On any interruption, Resume continues from that exact PK with no data loss and no delete.

[RESUME] ClaimLine: PK checkpoint found at 987,432,000 — resuming from there.
[PK-RANGE] ClaimLine: batch committed — last PK = 988,000,000 (568,000 rows).

LOB-aware automatic chunk sizing

For tables with nvarchar(max), varbinary(max), or xml columns, the chunk size is automatically reduced so each INSERT batch stays under 256 MB. A 100,000-row default chunk on a 13 KB/row table would be 1.3 GB per batch — the tool reduces this to ~20,164 rows automatically. No configuration needed.

BIGINT overflow pre-flight

Before seeding begins, the tool queries MAX(pk_col) for every INT PK column. If the value exceeds or approaches 2,147,483,647 (SQL Server INT_MAX), a warning is logged recommending the target column be altered to BIGINT before data copy begins.

Two-lane parallel copy

Small tables (below the Large table threshold, default 10M rows) run concurrently in a configurable thread pool. Large tables run sequentially after the fast lane completes. Tables whose row count cannot be determined from catalog stats are always placed in the slow lane as a safety measure.

BACPAC seed for 1 TB+ tables

For tables too large for row-by-row copy (e.g. large JSON/LOB tables in a 1.41 TB+ database), use the BACPAC Seed (Large Tables) button in the Migration tab. This uses Microsoft's SqlPackage CLI to export and import the table as a .bacpac file. Run a CDC or watermark sync pass afterward to catch any changes that occurred during the export/import window.

Azure SQL CDC LOB truncation workaround

Azure SQL Database (single DB) hard-codes max text repl size = 64 KB. Native CDC silently truncates any LOB value larger than 64 KB with no error. The tool automatically detects tables with LOB columns on Azure SQL DB sources and downgrades them to watermark sync, bypassing CDC entirely for those tables.

ChallengeScale (production environment)How Endrias Bridge handles it
Interrupted migration restarting from row 01.07 B-row ClaimLineModifierPK-range checkpoint — resumes from last committed PK
OOM crash on wide-LOB rows1.41 TB total DB, JSON/LOB tables ~13 KB/row avgAuto LOB chunk sizing — caps each batch at 256 MB
INT PK overflow4.13 B-row EditorOverrideFilterItemValuePre-flight warning, recommends BIGINT on target
Azure SQL CDC truncates LOB > 64 KBAll nvarchar(max) tablesAuto-downgrade to watermark sync for LOB tables
Table too large for row copyLarge JSON/LOB tables in 1.41 TB+ DBBACPAC seed via SqlPackage + CDC catch-up
Small tables blocked behind large tablesAll reference/lookup tablesTwo-lane parallel copy — small tables run immediately

📊 Comparison Report Shows Many "TARGET ONLY" Objects

After running the comparison tool you see hundreds of TARGET ONLY rows for stored procedures, functions, and table types — but zero SOURCE ONLY rows and all table row counts MATCH. Example:

Stored Procs,coreapi.PublishConfiguration,—,exists,TARGET ONLY Stored Procs,priceapi.PublishFeeSchedule,—,exists,TARGET ONLY Types,coreapi.tvpConfiguration,—,exists,TARGET ONLY ... (hundreds more) ... Tables,auditdata.Claims,13883883,13883883,MATCH Tables,analyticsdata.AggregateByEditFact,13972964,13972964,MATCH

This is not a migration failure. The TARGET ONLY entries are application-layer objects that belong to schemas deployed by your SSDT project or application installer — not by MigrationBridge. They exist on the shared RDS target because the application stack was already deployed there, but they are not present in the source audit/stats database being compared.

💡
A comparison report is a PASS when: (1) zero SOURCE ONLY rows, (2) all table row counts MATCH, (3) all columns/constraints/indexes MATCH, and (4) TARGET ONLY entries are limited to the application-deployed schemas listed below. No investigative action is needed for those TARGET ONLY rows.

Application schemas that legitimately produce TARGET ONLY entries

SchemaContains
coreapiConfiguration API procs & TVPs
priceapiFee schedule API procs, functions & TVPs
customapiCustom edit API procs & TVPs
referenceapiAge DX code API procs & TVPs
catalogapiCustom CPT/HCPCS API procs & TVPs
mappingapiMessage map API procs & TVPs
limitsapiFrequency limit API procs & TVPs
rulesapiMUE API procs & TVPs
dboShared TVP aliases
utilUtility procs, functions, types

When to investigate TARGET ONLY entries

Only flag a TARGET ONLY entry if it falls under a schema that was part of the migration (e.g. auditdata, analyticsdata, salesdata, configdata). That would indicate an object was created on target by something other than MigrationBridge (e.g. a manual hotfix) and the source and target have diverged.

📜 Reading the Migration Log

The Migration tab's log panel is the most useful diagnostic tool for connection problems. After a failed Load Tables, Assessment, or Start Migration, look for a block like this:

ERROR loading tables: [Microsoft][ODBC Driver 17 for SQL Server] Invalid connection string attribute (0); [08001] ... Connection: SQL Server: host='localhost' port='1433' database='AppDb' user='sa'

The Connection: ... line shows exactly what Endrias Bridge read from Step 5 for that side (source or target), using Python's repr() so hidden characters are visible:

What you seeWhat it means
host='localhost\n'A trailing line break was pasted into Host/IP — re-type the field.
host='Server=tcp:...;Database=...'A full connection string was pasted into Host/IP — see Cause 1 above.
database=' AppDb'A leading space in the Database field — re-type it.
port=''The Port field is empty — fill in the correct port (1433 for SQL Server, 5432 for PostgreSQL, etc.).

The password is never shown in this line — it's safe to copy this line and share it when asking for help.

📝 Step 5 Field Reference

Correct examples for each field, using SQL Server as the example database type:

FieldCorrect exampleCommon mistakes
DB TypeSQL Server
Host/IPlocalhost, 10.0.0.5, myserver.database.windows.net Pasting a full connection string; including tcp:, http://, or a port number here.
Port1433Leaving blank; including extra text like "1433 (default)".
DatabaseAppDbIncluding the server name; trailing spaces from pasting.
Usernamesa or your SQL login nameIncluding @servername unless your auth method requires it (Azure SQL).
Password(type as-is, including special characters)None — special characters are handled automatically.

🆘 Still Stuck?

If you've worked through the sections above and a connection still fails, gather the following before asking for help:

  1. The exact error text

    Copy the full error from the Migration tab log, including the Connection: ... line.

  2. What "Test Connection" reported

    On Step 5, click Test Connection for the affected server and note whether it says the port is reachable.

  3. Database type and host (no credentials)

    Note the DB Type and Host/IP value (e.g. "SQL Server, myserver.database.windows.net") — never share the password.

Never share passwords, connection strings, or API keys when reporting an issue — even to an AI assistant. If a credential is ever pasted somewhere it shouldn't be, rotate it immediately.

Index Rebuild Errors on PostgreSQL / MySQL

After bulk copy completes, the tool attempts to recreate secondary indexes that were dropped before the INSERT phase. Errors here do not roll back data — all rows are present; only the index is missing.

Common causes & fixes

ErrorCauseFix
ERROR: index "ix_foo" already exists (PostgreSQL) Re-run of a migration that completed index creation on a previous attempt Auto-fixed in Change 79. The rebuild path now runs DROP INDEX IF EXISTS before CREATE INDEX CONCURRENTLY — re-runs are idempotent. If you see this error with an older build, run DROP INDEX IF EXISTS "ix_foo"; manually then retry.
ERROR: canceling statement due to conflict with recovery (PG standby) CREATE INDEX CONCURRENTLY on a hot-standby is not allowed Run the CREATE INDEX directly on the primary replica, or set hot_standby_feedback = on
MySQL: Duplicate key name Index exists from a previous partial run DROP INDEX `ix_name` ON `table`; then retry
MySQL: [idx] skipped (XML/spatial/fulltext indexes cannot be created on MySQL) SQL Server XML indexes (PXML_*, XMLPATH_* etc.), fulltext, or spatial indexes were in the source schema Auto-handled in Change 79. These indexes are skipped automatically with a log note — no action required. They have no MySQL equivalent.
[idx] SKIP table.ix: no DDL saved — recreate manually The PG index definition could not be captured at disable time Recreate the index manually using the original DDL from the source schema

To view all indexes that need recreation, check the Migration Log for [idx] SKIP lines.

Per-Table Chunk Size Override

Use [chunk_overrides] in migrationbridge.conf to force a specific row batch size for tables that are too wide (causing OOM) or too narrow (running slowly).

How to add an override

  1. Open C:\MigrationTool\migrationbridge.conf in Notepad
  2. Add or find the [chunk_overrides] section
  3. Add a line: TableName = 500 (table name is case-insensitive)
  4. Save and restart MigrationBridge — no service restart needed for this setting
[chunk_overrides] BigLobTable = 500 AggregateLineFact = 200000 PerformanceAppHistory = 1000

The override takes priority over both the global setting and LOB-aware auto-sizing. The Migration Log shows: [TableName] per-table chunk override: 500 rows when active.

Recommended sizes: Wide LOB tables (avg row >50 KB) → 100–500. Narrow index tables → 50,000–200,000.

🔌 Oracle: Connection Diagnostics & Driver Setup

When Oracle is selected as source or target and you click Test Connection, Endrias Bridge now runs a pre-flight diagnostic before attempting the network connection. Instead of a generic error message, you see a detailed status dialog:

CheckWhat it does
Host reachability2-second socket connect to host:1521
Python driverTries import cx_Oracle, then import oracledb; shows version if found
Oracle Instant ClientWalks every directory in PATH and ORACLE_HOME looking for oci.dll (Windows) or libclntsh.so (Linux)

The dialog shows ✓/✗ for each check and provides three action buttons: [Install Python Driver] (copies pip install oracledb to clipboard), [Download Oracle Instant Client] (opens Oracle download page), [Open Installation Guide] (opens oracledb documentation).

How to install (one command)

  1. Open a terminal (Command Prompt or PowerShell)
  2. Run: pip install oracledb
  3. Restart MigrationBridge and click Test Connection again

oracledb thin mode connects directly to Oracle Database 12.1+ without Oracle Instant Client. MigrationBridge automatically uses oracle+oracledb:// as the SQLAlchemy dialect when oracledb is installed.

⚠ cx_Oracle does not build on Python 3.13
The legacy cx_Oracle package fails to compile on Python 3.13+ (ModuleNotFoundError: No module named 'pkg_resources'). Do not install cx_Oracle — use oracledb instead. MigrationBridge will fall back to cx_Oracle only if oracledb is absent and the Python version is ≤ 3.12.
DriverPython 3.13?Instant Client needed?Oracle versionStatus
oracledb✓ YesNo (thin mode)12.1+Recommended — use this
cx_Oracle✗ No — build failsYes — 64-bit Basic pkg11g+Legacy only (Python ≤ 3.12)

Common Oracle connection errors

Error codeMeaningFix
DPI-1047Python driver found, Instant Client DLL not found on PATHAdd Instant Client folder to PATH, or switch to oracledb thin mode
ORA-01017Invalid username or passwordCheck credentials; Oracle passwords are case-sensitive (11g+)
ORA-12154TNS: could not resolve service nameUse Easy Connect format in the DB field: hostname:port/service_name
ORA-12541No listener at host:portRun lsnrctl status; start listener if down; check firewall port 1521
ORA-12170Connect timeoutCheck host/IP, Oracle DB server running, firewall allows port 1521

🐛 Oracle: ORA-00942 on FK Constraints (Cross-Schema REFERENCES)

After schema creation succeeds, FK replay fails with:

ORA-00942: table or view does not exist ALTER TABLE "Purchasing"."Vendor" ADD CONSTRAINT … REFERENCES "Person"."BusinessEntity"

The parent table does exist. Oracle is reporting a privilege error as "does not exist" to avoid leaking schema information.

Root cause

Oracle requires an explicit REFERENCES object-level privilege when a FK crosses schema boundaries. Unlike within a single schema, Oracle does not automatically grant cross-schema FK access. The migration engine must issue:

GRANT REFERENCES ON "Person"."BusinessEntity" TO "Purchasing";

before the ALTER TABLE that references it. If the grant is skipped or targets the wrong user, Oracle silently raises ORA-01749 ("cannot grant to yourself") and the ALTER TABLE that follows gets ORA-00942.

What Endrias Bridge does automatically

Since Change 99, the FK replay loop parses each ALTER TABLE "child_schema"… REFERENCES "parent_schema"."parent_table" statement and issues the GRANT to the child schema before applying the constraint. The migration log line:

[FK] Granted REFERENCES on 53 parent→child pair(s)

confirms the grants were applied. All 90 FKs in AdventureWorks2019 apply cleanly.

If you still see ORA-00942 on FKs

CheckFix
The connecting user is not a DBA and cannot GRANT to other schemas Connect as SYSTEM or a DBA-privileged user for the migration run. The GRANT step requires the ability to grant object privileges across schemas.
Parent table not yet created when FK runs This should not happen (schema is created in FK-safe topological order). If it does, run Re-apply Schema Objects after the full migration completes.
Identifier case mismatch — parent schema name quoted differently Confirm Oracle stores schema usernames matching the quoted names in the DDL. Oracle user names are case-sensitive when double-quoted. Use UPPER-case schema names if in doubt.

🐛 Oracle: ORA-22848 CLOB in ORDER BY / Verification False MISMATCH

During post-migration verification of tables with NVARCHAR(MAX), VARBINARY(MAX), or XML columns, the log may show:

ORA-22848: cannot use CLOB type as comparison key

or, if the table's only primary key is a hierarchyid column:

MISMATCH Production.Document source=13 target=13 mismatched=6

even though all 13 rows were copied correctly.

Root cause

Oracle forbids LOB columns (CLOB, BLOB) in ORDER BY clauses. SQL Server's NVARCHAR(MAX) maps to String(length=None) in SQLAlchemy, which Oracle renders as CLOB. When the verification engine falls back to positional row ordering (used when all PK columns are of an unmappable type such as hierarchyid), it must exclude these columns from the ORDER BY.

If all PK columns are skipped (hierarchyid → NullType → excluded), positional ordering on SQL Server and Oracle diverges, causing row 0 on source ≠ row 0 on target — a false mismatch.

What Endrias Bridge does automatically (since Change 99)

These cases are handled automatically. If you see ORA-22848 in older builds, upgrade to the latest source and re-run verification via Re-apply Schema Objects or restart the migration.

📖 Oracle: Temporal Tables — Flashback Data Archive and PERIOD FOR SYSTEM_TIME

When migrating a SQL Server source with system-versioned temporal tables to an Oracle target, the data (both the live table and the history table) is migrated as plain tables. Oracle has no GENERATED ALWAYS AS ROW START/END before version 23ai, so period columns (SysStart, SysEnd) arrive as plain TIMESTAMP columns.

The migration log flags each period column:

[ORA-COMPAT] dbo.OrderLine.SysStart: temporal period column migrated as plain TIMESTAMP — GENERATED ALWAYS not supported on Oracle before 23ai; re-enable via FDA or PERIOD FOR SYSTEM_TIME post-migration (see Schema Objects tab)

Re-enabling history tracking on Oracle

Open the Schema Objects tab after migration, expand TEMPORAL TABLES, and select the table. The panel on the right shows a three-path script — choose the path that matches your Oracle version:

Oracle versionPathWhat to run
23ai+ Path A ALTER TABLE … ADD PERIOD FOR SYSTEM_TIME + ADD VERSIONING USE HISTORY TABLE. Points at the already-migrated history table so pre-migration history is immediately queryable.
12c–21c Path B CREATE FLASHBACK ARCHIVE fba_<table> + ALTER TABLE … FLASHBACK ARCHIVE. FDA auto-tracks all post-migration DML. The migrated history table covers pre-migration history.
11g R2 Path C Same as Path B without the OPTIMIZE DATA clause (12c only).

Required privileges for FDA

Notes on migrated data

CDC Lag Warning — Target Falling Behind

During CDC Stream mode, if the source database is producing changes faster than the tool can apply them to the target, you will see:

[stream] WARNING: high change rate (614 changes/s over last 58s) — target may fall behind.

This does not mean data is lost — the CDC log retains all changes. It means the replication lag is increasing.

What to check

BottleneckHow to diagnoseFix
Target IOPS saturated Check target DB server CPU/IO — is disk at 100%? Scale up target instance (RDS: change instance type or increase Provisioned IOPS)
Network latency source → target Ping or traceroute from migration host to target host Run migration host in same VPC/region as target
Source change burst (batch job running) Check source DB activity — is a batch ETL running? Pause the CDC stream until the burst subsides, then resume
Too many tables in CDC stream Log shows many tables each cycle Split into two CDC stream jobs with fewer tables each

The warning threshold is 500 changes/s sustained over 5+ consecutive 60-second windows. The rate is shown on every cycle log line: rate=127/s.

CDC Schema Drift — Table Suspended

If a column is added to or removed from a source table while the CDC stream is running, the affected table is automatically suspended and you will see:

[stream] *** SCHEMA DRIFT on ClaimHeader: added: AuditTimestamp; — table suspended. Re-enable with a schema refresh. ***

No data is written to the target for that table after the drift is detected. All other tables in the stream continue uninterrupted.

Recovery steps

  1. Stop the CDC stream (click Stop Stream in the GUI)
  2. Apply the matching schema change to the target table (e.g. ALTER TABLE ClaimHeader ADD AuditTimestamp DATETIME2)
  3. Go to the Schema Objects tab → Re-apply Schema Objects to sync the target DDL
  4. Restart the CDC stream — it will re-snapshot column sets for all tables
  5. The stream will pick up from the last saved LSN checkpoint; no rows are missed

The schema drift check runs every 20 cycles (approximately every 5 seconds at default poll rate). A column rename is seen as one removal + one addition.

🔒 PostgreSQL: "server does not support SSL"

Error: server does not support SSL, but SSL was required

This happens when you use a local or VM-based PostgreSQL server that does not have SSL configured, but the connection string requests sslmode=require.

Fix (already applied in current build)

Endrias Bridge now uses sslmode=prefer for PostgreSQL (VM / On-Prem). This tries SSL first and silently falls back to a plain connection — no error on either setup.

Database type selectedSSL mode usedWorks on
PostgreSQL (VM / On-Prem)sslmode=preferLocal, VM, container, dev laptop
AWS RDS for PostgreSQLsslmode=requireRDS (SSL enforced)
Azure Database for PostgreSQL Flexiblesslmode=requireAzure (SSL enforced)
Amazon Aurora (PostgreSQL)sslmode=requireAurora (SSL enforced)
GCP Cloud SQL (PostgreSQL)sslmode=requireGCP (SSL enforced)

If you still see the error

SQL Server: TCP timeout / server not found

Error: 08001 — TCP Provider: The wait operation timed out
or: A network-related or instance-specific error has occurred

SQL Server is unreachable from this machine. Work through this checklist in order:

  1. Correct host? — Check the Host/IP field. For named instances use HOSTNAME\INSTANCENAME. For Azure SQL use server.database.windows.net.
  2. Service running? — SQL Server Configuration Manager → SQL Server Services → confirm status is Running.
  3. TCP/IP enabled? — SQL Server Configuration Manager → Protocols for MSSQLSERVER → TCP/IP → Enabled. Restart the service after changing.
  4. Firewall rule?
    • Windows Firewall: inbound rule → TCP 1433
    • AWS: Security Group inbound rule → TCP 1433 from your IP
    • Azure: NSG inbound rule → TCP 1433 from your IP
  5. Remote connections allowed? — SSMS → Server Properties → Connections → Allow remote connections to this server must be checked.
  6. Port mismatch? — Named instances often use a dynamic port. Find the actual port in SQL Server Configuration Manager → TCP/IP Properties → IP Addresses → IPAll → TCP Dynamic Ports.
  7. SQL Server Browser? — For named instances, SQL Server Browser service must be running for port discovery.

The Test Connection dialog in Endrias Bridge shows your public IP — use it to add the correct firewall rule when connecting remotely.

🔒 MySQL / MariaDB: SSL errors

Error: SSL connection error or Commands out of sync

Local MySQL and MariaDB installations typically do not have SSL configured. Endrias Bridge uses ssl_disabled=true for MySQL (VM / On-Prem) and MariaDB, so no SSL negotiation is attempted.

Database typeSSL behavior
MySQL (VM / On-Prem)SSL disabled — plain connection
MariaDBSSL disabled — plain connection
AWS RDS for MySQLpymysql default — negotiates SSL automatically
Azure Database for MySQL Flexiblepymysql default — negotiates SSL automatically
Amazon Aurora (MySQL)pymysql default — negotiates SSL automatically

If you still see SSL errors: confirm you selected MySQL (VM / On-Prem) or MariaDB rather than a cloud variant.

🌐 General connection checklist (all databases)

Before running a migration on a new machine, verify each item:

CheckHow to verify
Host / IP correctPing the host from this machine: ping hostname
Port reachableUse Test Connection in the wizard — it probes the TCP port first
Firewall allows your IPYour public IP is shown in the connection error dialog — add it to the cloud firewall / NSG / Security Group
Correct DB type selectedOn-prem types (VM / On-Prem) use permissive SSL; cloud types use strict SSL. Mismatch causes SSL errors.
ODBC Driver 17 installedRequired for SQL Server connections. Download: Microsoft ODBC Driver 17 for SQL Server
psycopg2 installedRequired for PostgreSQL. Included in the Endrias Bridge installer.
pymysql installedRequired for MySQL / MariaDB / Aurora MySQL. Included in the installer.
Database existsSQL Server error 4060 = database not found. PostgreSQL: FATAL: database "x" does not exist.
User has login permissionSQL Server 18456 = auth failed. PostgreSQL fe_sendauth = auth failed.

🔃 CT Sync — Change Tracking Issues

Error: "Change Tracking is not enabled on database"

Cause: CT Sync requires Change Tracking enabled at the database level before Bridge can read deltas.

Fix: Run on the source Azure SQL database (requires ALTER DATABASE permission):

ALTER DATABASE [YourDatabase] SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 3 DAYS, AUTO_CLEANUP = ON);

Error: "Change Tracking is not enabled on table [schema].[table]"

Cause: CT enabled at DB level but not on the individual table.

Fix:

ALTER TABLE [schema].[YourTable] ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF);

Warning: "CT version X is older than min_valid_version Y — reseed required"

Cause: The stored CT version checkpoint is older than SQL Server's minimum valid version. This happens when the CT retention window (default: 2–3 days) expires between CT Sync runs. The delta data for those rows no longer exists.

Fix: Run a Full migration for the affected tables to reseed them, then re-enable CT Sync. In the Bridge Migration tab, select only the affected tables, set mode to Full, run migration, then switch back to CT Sync.

CT Sync applies 0 changes even though rows were modified on source

Cause: The table has CT enabled but no changes have occurred since the last stored version. Or the stored version equals the current CT version (no new changes).

Resolution: This is expected behavior — CT Sync logs "0 changes" when the source is idle. Insert a test row on source, run CT Sync again, and confirm the row appears on target.

CT Sync missing DELETE operations

Cause: CT only records which PKs were inserted/updated/deleted — it does not capture the before-image of deleted rows. Bridge uses the PK list from CHANGETABLE to issue DELETE WHERE PK IN (...) on the target. If the target row was already missing, the delete is a no-op.

Resolution: No action needed — idempotent behavior is correct.

CT Sync runs but target row counts diverge over time

Cause: Typically caused by CT retention window expiry between runs (see "CT version older than min_valid_version" above), or a table that has no PK (CT requires a PK to track changes).

Fix: Ensure all CT Sync tables have a primary key. Increase CT retention: ALTER DATABASE SET CHANGE_TRACKING (CHANGE_RETENTION = 7 DAYS). Schedule CT Sync runs more frequently than the retention window.

🤖 CLI Headless Mode Issues

Error: "No module named 'migrationbridge'" when running MigrationBridge migrate

Cause: Running the Python source directly without the installed EXE, or the EXE is not on PATH.

Fix: Use the full path to the installed EXE: "C:\Program Files\Endrias Bridge\MigrationBridge.exe" migrate .... Or add the install directory to PATH.

Exit code 1 but no visible error in console output

Cause: The error was written to the migration queue but the log drain thread ended before flushing. Use --report report.html to capture the full output to a file, which persists after the process exits.

CLI migration succeeds (exit 0) but verify returns mismatches

Cause: --verify flag was not passed — verification is opt-in in CLI mode. Without it, Bridge exits 0 on a clean data copy regardless of row counts.

Fix: Always pass --verify for production migrations: MigrationBridge migrate ... --verify --report report.html

Azure DevOps pipeline does not fail on migration error

Cause: The PowerShell task is not checking $LASTEXITCODE after the migrate command.

Fix: Add an explicit exit code check after the command:

MigrationBridge migrate --src-type azuresql ... --verify if ($LASTEXITCODE -ne 0) { Write-Error "Migration failed"; exit 1 }

Post-Migration Verification Issues

Verification reports row count mismatch on a table that appears complete

Cause: The source was still receiving writes during migration. Row counts are compared at verification time — if any rows were inserted between data copy and verification, the count will differ.

Resolution: This is expected for live databases. For a clean verification, perform the migration during a maintenance window or stop writes to the source tables before running. Re-run verification using the migration report's table list.

Verification reports checksum mismatch (rows match but checksums differ)

Cause: One or more columns have data type differences that cause different string representations — e.g., FLOAT precision rounding, DATETIME vs DATETIME2 microsecond handling, or trailing spaces in NCHAR fixed-length columns.

Resolution: Examine the type mapping in the pre-migration type scan. For float columns, checksum differences are cosmetic and row data is functionally identical. For NCHAR/CHAR differences, verify target column widths match source.

Verify checkbox has no effect in Migration Projects (batch jobs)

Cause: Verification is per-run opt-in; batch jobs do not inherit the GUI checkbox state.

Fix: Add do_verify = true to the job profile in migration_projects.json, or enable it explicitly when the job calls _do_migration. CLI jobs: pass --verify flag.

🔗 FK IntegrityError 23000 — FOREIGN KEY SAME TABLE Constraint (Self-Referential FK)

Symptom

Configurations: ERROR (BCP/PK-range) — (pyodbc.IntegrityError) ('23000', 'The INSERT statement conflicted with the FOREIGN KEY SAME TABLE constraint "FK_Configurations_Configurations". The conflict occurred in database "LConfiguration", table "configdata.Configurations", column \'ConfigurationId\'. (547)')

The key indicator is FOREIGN KEY SAME TABLE in the error message. This is a self-referential FK — a column on the table references the PK of the same table (e.g. Configurations.ParentConfigurationId → Configurations.ConfigurationId).

Root cause

PK-range copy inserts rows in ascending primary key order, but a row with a smaller PK may have a ParentConfigurationId pointing to a row with a larger PK that hasn't been inserted yet. The FK constraint fires on every INSERT batch, so even a single out-of-order parent reference in a 100,000-row batch will roll the whole batch back. Wave scheduling cannot help because parent and child are in the same table.

Fix (Change 47)

The migration tool now automatically detects self-referential FK constraints on each table before copying. When found, it issues ALTER TABLE … NOCHECK CONSTRAINT … on the target to suspend enforcement, copies all rows, then re-enables and validates with ALTER TABLE … WITH CHECK CHECK CONSTRAINT …. This is fully automatic — no configuration required. The log will show:

Configurations: self-ref FK disabled for bulk copy ([FK_Configurations_Configurations]) Configurations: 3,234 rows done (BCP) Configurations: self-ref FK re-enabled and validated

If the re-enable step fails (e.g. data integrity issue on the source), a WARNING is logged but the copy is not marked failed — investigate the data before relying on the constraint.

🔗 FK IntegrityError 23000 in Multi-Schema Databases (Same Table Name, Different Schemas)

Symptom

In databases containing two or more schemas where identically-named tables exist in both (e.g. errordata.ClaimHeader and historydata.ClaimHeader), child tables in the second schema fail with IntegrityError 23000 even though the fast-lane wave scheduler is enabled:

historydata.ClaimHeaderConditionCode: ERROR — Violation of FOREIGN KEY constraint 'FK_ClaimHeaderConditionCode_ClaimHeader'. The INSERT statement conflicted with the FOREIGN KEY constraint "FK_ClaimHeaderConditionCode_ClaimHeader". The conflict occurred in database "LHistory", table "historydata.ClaimHeader". (2627)

Root cause

Before Change 45, the wave scheduler used bare table names (ClaimHeader) as dictionary keys. When two schemas both contain a ClaimHeader table, both were assigned the same key, and the wave assignment for both was forced to wave 0 ("no fast-lane parents"). Wave 0 was considered complete when any one ClaimHeader finished — even if the other schema's copy was still running. Child tables in wave 1 then started immediately, racing ahead of the still-active parent.

Fix (Change 45)

Update to a build containing Change 45. The wave scheduler now uses schema-qualified keys (schema.name) for every internal dictionary, so errordata.ClaimHeader and historydata.ClaimHeader are tracked independently. Wave completion is gated on all tables in that wave — across all schemas — before the next wave starts.

After applying the fix, clear the checkpoint file (or use Start Migration rather than Resume) so the scheduler re-evaluates wave assignments with the corrected keys. Affected databases (like AppHistoryRT) should be re-migrated from the beginning.

💡
If you still see IntegrityError 23000 after applying Change 45, check whether the parent table is in the slow lane (large table, sequential). Slow-lane tables run after all fast-lane waves complete, so child tables of a slow-lane parent will be in the slow lane too — they cannot race the parent.

Resume Restarts from PK=1 — All Rows Re-Inserted, Duplicate-Key Errors

Symptom

After clicking Resume Migration, the log shows a table starting from very low PK values (e.g. PK=19) even though thousands of rows are already on the target and the previous run stopped at a much higher checkpoint. Hundreds of duplicate-key errors follow:

Configurations: ERROR (BCP/PK-range) — (pyodbc.IntegrityError) ('23000', "...Violation of PRIMARY KEY constraint 'PK_Configuration'...The duplicate key value is (19)...")

Root Cause (3 compounding bugs — fixed in Change 48)

  1. Resume thread missing state-file scope: _do_resume_migration() runs on a new threading.Thread. Thread-local storage (_tl.state_file) is per-thread — the GUI thread's call to set_active_database() is invisible here. Without the scope set, every load_pk_checkpoint() silently reads the empty legacy file and returns None.
  2. Missing schema on checkpoint calls: The resume path called load_pk_checkpoint(table_name) without passing schema=. The key defaulted to dbo.TableName even for schema-qualified tables like configdata.Configurations. Checkpoint was saved as configdata.Configurations but looked up as dbo.Configurations — always a miss.
  3. _text NameError on self-ref FK disable: Change 47's self-ref FK code used _text() which is only imported inside a sibling method. The exception was caught and silently cleared — but it also cleared _self_ref_fks, which may have affected downstream state.

Fix

Update to a build containing Change 48. No state-file cleanup is needed — the MAX(pk) fallback correctly derives and saves the checkpoint on the first Resume after the fix, and copy proceeds from last_pk + 1.

If you ran multiple failed Resume attempts against the same table, confirm that no duplicate rows were committed on the target before resuming. The PK-range copy skips PKs already in the checkpoint, but rows inserted during failed batches (below the checkpoint) may already be present. A quick SELECT COUNT(*) vs source is sufficient — if counts match, the table is done and will be SKIPped on next Resume.

📅 Temporal Table Error 13575 — ADD PERIOD FOR SYSTEM_TIME Fails After Migration

Symptom

After clicking the Temporal Tables button in the Schema Objects tab, the log shows error 13575 for one or more tables:

rulesdata.CustomMue: system-versioning FAILED — (pyodbc.ProgrammingError) ('42000', '... ADD PERIOD FOR SYSTEM_TIME failed because table ''LPubconfig2.rulesdata.CustomMue'' contains records where end of period is not equal to MAX datetime. (13575) ...') [SQL: ALTER TABLE [rulesdata].[CustomMue] ADD PERIOD FOR SYSTEM_TIME ([SysStart], [SysEnd])]

Root Cause

The Change 46 ODBC Driver 17 workaround clamps any datetime at 9999-12-31 23:59:59 with non-zero microseconds to exactly 9999-12-31 23:59:59.0000000. For normal data columns this is correct. Temporal tables store current row markers as SysEnd = 9999-12-31 23:59:59.9999999 (SQL Server's MAX datetime2). After migration those rows carry .0000000, and ADD PERIOD FOR SYSTEM_TIME requires exact MAX datetime2 on all current rows.

Fix

Update to a build containing Change 49. The tool now emits an UPDATE step that restores SysEnd = '9999-12-31 23:59:59.9999999' on all clamped rows immediately before ADD PERIOD. Re-run the Temporal Tables button — affected tables will succeed.

💡
This issue only affects tables that have already been migrated with the Change 46 build and have not yet had system-versioning re-enabled. Tables where system-versioning was successfully applied before this session are not affected.

Temporal Table Error 13537 — Cannot Update GENERATED ALWAYS Columns

Symptom

After running the Schema Objects → Temporal Tables button (or the auto-reenable at the end of migration), the log shows:

Cannot update GENERATED ALWAYS columns in table 'dbo.SomeTemporalTable'. Msg 13537, Level 16, State 1

The fix_sysend UPDATE step fails even though SYSTEM_VERSIONING = OFF. The step that re-enables versioning never runs.

Root Cause

When SYSTEM_VERSIONING is turned OFF the PERIOD FOR SYSTEM_TIME definition is not automatically removed. A PERIOD definition marks period columns as GENERATED ALWAYS, which SQL Server enforces even while versioning is off. The fix_sysend UPDATE tries to write to those columns and SQL Server raises error 13537.

Fix

Update to a build containing Change 53. The generate_temporal_table_script() function now issues a DROP PERIOD FOR SYSTEM_TIME step before the UPDATE:

IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID(N'[schema].[table]'))
    ALTER TABLE [schema].[table] DROP PERIOD FOR SYSTEM_TIME;

Then re-adds the PERIOD and re-enables versioning as steps 4 and 5. Run the Schema Objects → Temporal Tables button again after updating.

📅 Error 22007 — Invalid date format on 9999-12-31 23:59:59 with Zero Microseconds

Symptom

Resume Migration fails with 22007 Invalid date format on a table that contains a datetime/datetime2 column. Inspecting the bound parameters shows datetime.datetime(9999, 12, 31, 23, 59, 59)no microseconds:

AspNetUsers: ERROR — (pyodbc.DataError) ('22007', '[22007] [Microsoft][ODBC Driver 17 for SQL Server]Invalid date format (0) (SQLParamData)') -- bound value: datetime.datetime(9999, 12, 31, 23, 59, 59) ← microsecond=0

Root Cause

ODBC Driver 17 with fast_executemany=True rejects any value at the 9999-12-31 23:59:59 second boundary, including exactly .000000. Change 46 added a guard and microsecond > 0 that only caught fractional-second values; source rows where .NET stores DateTime.MaxValue as exactly 9999-12-31 23:59:59 bypass the clamp unchanged.

Fix

Update to a build containing Change 54. The microsecond > 0 guard has been removed so all 9999-12-31 23:59:59.* values are clamped to 9998-12-31 23:59:59.999999. Restart the tool and click Resume Migration.

🚫 BACPAC Seed: NativeError 208 — Invalid object name 'schema.Table'

Symptom

BACPAC Seed's bcp import step fails immediately after export with:

bcp in → [coredata].[Configuration] (TableData-000-00000.BCP) SQLState = S0002, NativeError = 208 Error = [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Invalid object name 'coredata.Configuration'. ERROR (import): [coredata].[Configuration] (TableData-000-00000.BCP): SQLState = S0002, NativeError = 208

Root cause

bcp issues bcp schema.table in ... against the target database. If the table does not exist on the target yet, the server returns error 208. This almost always means BACPAC Seed was run before the main migration — the target database has no tables at all, or is missing that specific table.

Fix

  1. Run Run Migration (main migration, all tables, schema + data) first.
  2. Verify the table exists on target: SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Configuration'
  3. Then re-run BACPAC Seed — use Import Only mode if .bacpac files are already on disk to skip the long re-export.

See Jobs Guide — Part 3B for the complete correct order of operations.

🔴 BACPAC Seed: Export Exited With Code 1 (SqlPackage)

Symptom

ERROR (export): SqlPackage export exited with code 1

Common causes

Fix

If the table's data was already copied by the main migration, you do not need to export it again. Use Import Only mode — if you have a .bacpac from a previous run it loads from disk. If the .bacpac is missing, the table's data is already on the target from the main migration PK-range copy, so BACPAC Seed is not needed for it.

BACPAC Import: SQLState 22001 — String Data Right Truncation

Symptom

bcp in → [coredata].[ProviderType] (TableData-000-00000.BCP) SQLState = 22001, NativeError = 0 Error = [Microsoft][ODBC Driver 17 for SQL Server]String data, right truncation BCP copy in failed ERROR (import): [coredata].[ProviderType] (TableData-000-00000.BCP): SQLState = 22001, NativeError = 0

Root cause

SqlPackage writes .bacpac BCP data files in character encoding (text format). If bcp is invoked with -n (native/binary mode), it misreads column boundaries and tries to stuff more bytes than fit into a target column — even when declared VARCHAR(n) lengths are identical on source and target. This is a bcp mode mismatch, not a schema mismatch.

Checking column lengths will show they match — the problem is bcp mode, not column width. Do not drop and recreate the table; switch to character mode instead.

Fix (permanent — code change 72)

Update to a build containing Change 72. The format-file generation and bcp fallback both now use -c (character mode) instead of -n:

# Format file generation — character mode
bcp schema.table format nul -f table.fmt -x -c -S server -d db

# bcp import with format file
bcp schema.table in TableData-000-00000.BCP -f table.fmt -S server

# Fallback (no format file) — character mode
bcp schema.table in TableData-000-00000.BCP -c -S server

Workaround (if you cannot update the build)

The affected table's data is already on the target from the main migration PK-range copy. Skip BACPAC Seed for that table — it is not needed.

🔒 BACPAC Seed: Export Fails — "File is being used by another process"

Symptom

*** Error exporting database: Could not save package to file. The process cannot access the file '...\coredata_CustomEditOverrideFilterItemValue.bacpac' because it is being used by another process. Time elapsed 0:00:15.28 ERROR (export): SqlPackage export exited with code 1

Root cause

A previous run of SqlPackage crashed or was killed mid-export. Windows still holds a file lock on the partially-written .bacpac. The next SqlPackage run cannot open the file to write.

Fix (automatic — code change 72)

Update to a build containing Change 72. The tool now detects the "being used by another process" message, deletes the stale .bacpac, and retries the export automatically:

Stale .bacpac file removed — retrying export...

Fix (manual)

  1. Find and delete the locked .bacpac file:
    Remove-Item "...\coredata_CustomEditOverrideFilterItemValue.bacpac" -Force
  2. If the file refuses to delete, identify the locking process:
    handle64.exe coredata_CustomEditOverrideFilterItemValue.bacpac
    (Sysinternals Handle tool, or use Resource Monitor → CPU → search handles)
  3. Kill the locking process, then delete the file.
  4. Re-run BACPAC Seed.
💡
If disk space is low (see Errno 28), the large .bacpac file may have caused the crash. Free space first, then retry.

Migration / BACPAC: FK 2714 "There is already an object named…" — False Warning

Symptom

Schema: FK recreate WARNING — There is already an object named 'FK_ProviderType_Configuration' in the database. (2714) [SQL: ALTER TABLE [coredata].[ProviderType] ADD CONSTRAINT [FK_ProviderType_Configuration] FOREIGN KEY ...]

Root cause

During schema creation, the tool temporarily drops a small set of FKs (those that cross-reference tables being created in dependency order), copies data, then tries to recreate them. On a re-run — where the target already has data and constraints from a previous migration — the FKs were never actually dropped by this run because the table was created fresh with the FK inline. The recreate attempt then hits 2714 "already exists."

Is this a problem?

No. Error 2714 means the constraint is already present on the target — exactly the desired state. The FK is enforced and protecting referential integrity.

Fix (automatic — code change 72)

Update to a build containing Change 72. The tool checks for error code 2714 and logs it as informational instead of WARNING:

Schema: FK already exists (skipped) — FK_ProviderType_Configuration

💾 BACPAC Seed: [Errno 28] No Space Left on Device

Symptom

Export done: 0.00 rows/sec file size 6.93 GB ERROR (import): [Errno 28] No space left on device

Root cause

SqlPackage exported a very large table (e.g. CustomEditOverrideFilterItemValue at 6.93 GB) and exhausted the free space on the local drive where the .bacpac output directory is located. The bcp import process then cannot write temp files.

Immediate fix

  1. Delete the oversized .bacpac to recover disk space:
    Remove-Item "...\coredata_CustomEditOverrideFilterItemValue.bacpac" -Force
  2. Check free space: Get-PSDrive C
  3. If the table's data is already on the target from the main migration PK-range copy, BACPAC Seed is not needed for it — skip it.

Prevent recurrence

OptionDetail
Change output directoryPoint BACPAC Seed output to a drive with >20 GB free
Remove completed .bacpac filesDelete after successful import to reclaim space immediately
Use Import Only modeSkip re-export if a valid .bacpac is already on disk from a prior run
Rely on PK-range copyFor tables already copied by the main migration, BACPAC Seed is optional
Very large tables (billions of rows / multi-GB .bacpac) should be handled via the main migration PK-range path rather than BACPAC Seed. SqlPackage serializes the entire table into a single .bacpac before any import starts — requiring double the table's on-disk size in free space.

🔗 BACPAC CREATE TABLE Fails: FK 1767 — "Foreign key references invalid table"

Symptom

Schema: batch create failed — Foreign key 'FK_CustomEditOverrideFilterItemValue_CustomEditOverrideFilterItem' references invalid table 'dbo.RuleOverrideItem'. (1767) Schema: CustomEditOverrideFilterItemValue FAILED Schema: PricerParameter FAILED Schema: created 6/8 table(s), 2 failed

Root cause

BACPAC Seed was given a table list that includes a child table (CustomEditOverrideFilterItemValue) but the FK parent (CustomEditOverrideFilterItem) is not in scope for this BACPAC Seed run and does not yet exist on the target. SqlPackage embeds a FK pointing at a table that the target hasn't seen.

This happens when BACPAC Seed runs on a fresh target before the main migration creates all 154 tables.

Fix

  1. Run main migration first — all 154 tables including FK parents must exist on target before BACPAC Seed.
  2. Run BACPAC Seed after the main migration completes. The FK parents are present and table creation (or skip) succeeds.

See Correct order of operations for the complete sequence.

38 Temporal Tables FAILED During Re-apply Schema Objects (NativeError 208)

Symptom

Re-apply Schema Objects runs and every temporal table fails:

salesdata.CptMaxUnits: system-versioning FAILED - Invalid object name 'salesdata.CptMaxUnits'. (208) mappingdata.SystemMap: system-versioning FAILED - Invalid object name 'mappingdata.SystemMap'. (208) ... (38 tables total)

Root cause

Re-apply was triggered while the target was still missing the base tables. The tool tries to UPDATE [salesdata].[CptMaxUnits] SET SysEnd = ... before ALTER TABLE ... SET (SYSTEM_VERSIONING = ON), but the table doesn't exist because the main migration hasn't run yet.

Fix

  1. Complete the main migration (all tables, schema + data).
  2. Complete BACPAC Seed for the large tables.
  3. Then click Re-apply Schema Objects — all 38 temporal tables succeed because base tables now exist with data.
The temporal table data was successfully copied during the main migration — these errors do not mean data loss. Only the SYSTEM_VERSIONING = ON flag needs to be set, which Re-apply handles once the tables exist.

🔌 Main Migration: "BULK INSERT cannot access a local temp file — falling back to PK-range copy"

Symptom

Configuration: [bcp] target is a remote server — BULK INSERT cannot access a local temp file; use PK-range copy for RDS/Azure SQL targets — falling back to PK-range copy

This is not an error

Azure SQL Database PaaS does not permit BULK INSERT from a local file path. The tool detects this automatically and falls back to PK-range copy, which works correctly for all remote targets including Azure SQL and AWS RDS. No action required — the migration proceeds normally.

📋 BACPAC + Main Migration — Complete Step-by-Step Runbook

Use this runbook any time your migration includes tables that require BACPAC Seed (large tables with FK chains that bcp can't reach directly, or tables exceeding the PK-range throughput target). Following this exact order eliminates the common cascade of 208, 1767, 22001, and temporal-208 errors.

Do not run BACPAC Seed before the main migration. Every BACPAC failure pattern — NativeError 208, FK 1767, temporal-208 on Re-apply — traces back to BACPAC running before the main migration created the target tables.

Prerequisites

Step 1 — Run Main Migration (all 154 tables)

From the Migration tab, run a full migration with all tables selected. This creates every schema, table, index, and FK on the target, and copies all rows via PK-range.

Schema: created 154 table(s), FK-dependency order: ... --- Full copy: 1,341,210 row(s) in 22m 26s (997 rows/sec) ---

Expected: all 154 tables created, all constraint verifications OK, row count matching source. Temporal tables will show "versioning already OFF — skip" (correct — Re-apply will turn them back on later).

Step 2 — Deploy Code Fix (if not already on build ≥ 72)

Copy the updated db_migration.py to the running tool location:

Copy-Item "C:\MigrationT\Migrationtool\migrationbridge\gui\db_migration.py" `
  "<OneDrive path>\MigrationTool\migrationbridge\gui\db_migration.py" -Force

Restart the GUI after copying. This ensures the bcp -c fix, file-lock retry, and FK 2714 silencing are active for the BACPAC run.

Step 3 — Free Disk Space

Check available space on the BACPAC output drive before starting. A 6 GB table needs >12 GB free (export + temp files).

Get-PSDrive C

Delete any stale .bacpac files from previous runs:

Get-ChildItem "<output dir>" -Filter *.bacpac | Remove-Item -Force

Step 4 — Run BACPAC Seed

From the BACPAC Seed tab, select the tables and run. The tool exports each table via SqlPackage, strips parent-table data from the .bacpac, then imports via bcp.

Expected per-table log:

[1/N] coredata.Configuration Step 1/3: Exporting via SqlPackage → ...coredata_Configuration.bacpac Successfully exported ... Export done: file size 0.01 GB Step 2/3: Importing into target [coredata].[Configuration]: 241,210 rows already on target — skipping bcp import. Import done. Step 3/3: CDC catch-up: +0 rows (no_sync)

If a table was already copied by the main migration PK-range, the tool detects this and skips the bcp import (row-count guard). BACPAC Seed completes cleanly.

Success indicator:

=== BACPAC Seed DONE in 4m 06s ===

Step 4a — Handle "No space left on device" (if it occurs)

If a very large table exhausts disk space during export:

  1. Delete the oversized .bacpac: Remove-Item "...\coredata_<Table>.bacpac" -Force
  2. Verify the table is already on target from the main migration: SELECT COUNT(*) FROM target.schema.Table
  3. If row count matches source, remove that table from the BACPAC list and continue.
  4. If it doesn't match, increase free space (move output dir to a larger drive) and re-run.

Step 5 — Re-apply Schema Objects

From the Schema Objects tab, click Re-apply Schema Objects. This re-enables SYSTEM_VERSIONING on 38 temporal tables and recompiles all 166 views/functions/procedures/triggers.

Expected:

--- Table types: applying 93 type(s) --- Table types: applied 93, 0 failed --- Schema objects: applying 166 view(s)/function(s)/procedure(s)/trigger(s) --- Schema objects: applied 166 --- Temporal tables: re-enabling system-versioning for 38 table(s) --- salesdata.CptMaxUnits: system-versioning ON (history table: salesdata.CptMaxUnits_HISTORY) ... (38 tables, all ON) --- Re-apply schema objects: complete ---

If any temporal table shows FAILED with NativeError 208, the main migration has not yet created that table. Complete the main migration first and retry Re-apply.

Step 6 — Validate

Run these validation queries on the target database:

-- 1. Confirm all 38 temporal tables are SYSTEM_VERSIONED
SELECT OBJECT_SCHEMA_NAME(object_id)+'.'+name AS tbl, temporal_type_desc
FROM sys.tables
WHERE temporal_type = 2
ORDER BY 1;
-- Expected: 38 rows, all SYSTEM_VERSIONED_TEMPORAL_TABLE

-- 2. Row count spot checks (adjust table names for your database)
SELECT COUNT(*) FROM coredata.Configuration;      -- e.g. 241,210
SELECT COUNT(*) FROM coredata.ProviderType;        -- e.g. 1,100,000
SELECT COUNT(*) FROM dbo.RuleOverrideItemValue; -- must match source

-- 3. FK integrity spot check
SELECT TOP 5 * FROM coredata.ProviderType pt
JOIN coredata.Configuration c ON pt.ConfigurationId = c.ConfigurationId;
-- Returns rows → FK chain intact

-- 4. Function compiles (schema objects applied)
SELECT OBJECT_ID('ruleapi.GetRuleIdByConfigurationId');
-- Returns a non-NULL object_id → function exists

Error Quick-Reference

ErrorCodeRoot causeFix
Invalid object name 'schema.Table'208BACPAC ran before main migrationRun main migration first
String data, right truncation22001bcp using -n native mode on character-encoded .bacpacUpdate to build ≥72 (-c mode)
File being used by another processStale .bacpac from crashed runDelete .bacpac; auto-retried in build ≥72
FK references invalid table (1767)1767BACPAC creating child table before parent existsRun main migration first
Object already named… (2714)2714FK already on target; false alarm on re-runNot an error; silenced in build ≥72
No space left on deviceErrno 28Large .bacpac exhausted diskDelete .bacpac; use larger output drive
system-versioning FAILED (208) on Re-apply208Re-apply before main migration created tablesRun main migration first, then Re-apply

For the Jobs Guide version of this runbook (with EndriasBridge UI screenshots): Jobs Guide — Part 3B.

🔗 Self-Ref FK SAME TABLE Error Persists Even After Change 47 Is Applied

Symptom

Resume Migration shows the FK SAME TABLE IntegrityError on Configurations, but the log does not contain the "self-ref FK disabled for bulk copy" message — meaning the FK was never detected:

Configurations: ERROR (BCP/PK-range) — (pyodbc.IntegrityError) ('23000', '...The INSERT statement conflicted with the FOREIGN KEY SAME TABLE constraint "FK_Configurations_Configurations"...') ← no "self-ref FK disabled" line above this error

Root Cause (Change 50)

When SQLAlchemy reflects a self-referential FK, the fk.column.table object often has .schema = None even when the physical table lives in schema configdata. Change 47's detection used strict equality fk.column.table.schema == tbl.schema — which evaluates None == 'configdata'False — so the FK was silently skipped.

Fix

Update to a build containing Change 50. The comparison now normalizes both sides: (fk.column.table.schema or 'dbo').lower() == (tbl.schema or 'dbo').lower(). This correctly matches tables in the default schema (None or dbo) as well as named schemas like configdata.