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.
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 failed | Error pattern | Fix |
|---|---|---|
| 1/3 Source connection | Login timeout expired | Check 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 configured | Wizard Step 5 not completed — fill in all Source DB fields and re-run | |
| 2/3 Target connection | SSL SYSCALL / certificate verify failed | Add TrustServerCertificate=yes to the target connection string, or install the RDS CA cert bundle. See SSL certificate error (AWS RDS). |
| Database '…' does not exist | Endrias 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 eligibility | CDC eligibility … DISABLED | Warning 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
| Check | Status | Cause | Fix |
|---|---|---|---|
| Source connection | ✗ Failed | Connection string invalid or server unreachable | Fix wizard Step 5 source fields; verify network path |
| Target connection | ✗ Failed | Target DB unreachable or auth failure | Fix wizard Step 5 target fields; check security group / firewall |
| Source permissions | ⚠ Limited | Login not in db_datareader | ALTER ROLE db_datareader ADD MEMBER [your_login] on source DB |
| Target permissions | ✗ Failed | Login cannot CREATE TABLE on target | Grant CREATE TABLE, INSERT, UPDATE, DELETE, ALTER to target login; or add to db_owner |
| CDC eligibility | ⚠ Disabled | CDC not enabled on source | USE [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 | ✗ Critical | C:\ has < 5 GB free | Free disk space; or configure BACPAC output path to a larger volume with more available space |
| Staging disk space | ⚠ Low | C:\ has 5–20 GB free | Recommended ≥ 20 GB for BACPAC exports; consider clearing temp files |
| ODBC Driver | ✗ Missing | No SQL Server ODBC driver installed | Download and install ODBC Driver 17 for SQL Server from Microsoft; restart Endrias Bridge after install |
| ODBC Driver | ⚠ Older version | ODBC Driver 13 or earlier | Install Driver 17 or 18 alongside the older driver (both can coexist) |
| bcp.exe on PATH | ⚠ Missing | bcp.exe not found | Install 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
- Ensure wizard Step 5 has been visited at least once so connection strings are populated.
- Check the Error Log tab for any Python exception that occurred when building the Tools tab.
- Restart Endrias Bridge if the Tools tab appears blank.
Update Checker Issues
The update checker (Tools tab → Check for Updates) queries api.github.com/repos/bilenbiruk24/Migrationtool/releases/latest.
| Error / symptom | Cause | Fix |
|---|---|---|
| Check failed: <URLError: timed out> | No outbound internet / proxy blocking GitHub API | Ensure the staging host can reach api.github.com on port 443; configure proxy if required |
| Check failed: HTTP 403 / rate limit | GitHub 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 upgrade | Version constant in code not updated | Version is hard-coded as v1.2.0 in _update_check_worker; update it after each release |
| Button appears but does nothing | Background thread silently errored | Check 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 text | Dot color | Meaning | What to do |
|---|---|---|---|
| Idle | Grey | No operation running | Normal — ready to start |
| Migrating… | Amber | Migration in progress | Wait; monitor Migration Log |
| Pre-flight failed | Red | Pre-flight check failed | See Pre-flight check failed; fix and re-run |
| Migration complete | Green | Migration finished successfully | Verify row counts; proceed to Phase 4 hardening |
| Migration failed | Red | Fatal exception during migration | Check Migration Log for FATAL line; see Health Check to diagnose |
| Streaming (…) | Green | CDC replication stream active | Normal — stream is running |
| Schema drift detected | Amber | Source schema changed mid-stream | See CDC schema drift; acknowledge drift and restart stream |
| Health check running… | Amber | Health check in progress | Wait for results (usually < 10 s) |
| Ready to migrate | Green | All 8 health checks passed | Proceed with migration |
| Health: N failed | Red | One or more health check failures | Review health check Treeview; remediate failed rows |
| Health: N warnings | Amber | Non-critical health check warnings | Review 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:
-
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.
-
Put exactly one value per field
The Host/IP field takes only a hostname or IP address (e.g.
localhostor10.0.0.5) — never a full connection string, URL, orkey=value;key=valuefragment copied from another tool. -
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.
-
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
| Error | Cause | Fix |
|---|---|---|
| ODBC format error | Old 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 Server | Type check used db_type != 'mssql' but wizard returns 'Azure SQL Database' etc. | Update to v1.2.1 — now checks _SQLSERVER_FAMILY set |
| 42000 permission denied | sys.dm_os_volume_stats needs VIEW SERVER STATE; Azure SQL PaaS blocks it | v1.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 PaaS | Grant 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:
-
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.
-
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.
-
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.
-
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.
"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.
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
| Symptom | Fix |
|---|---|
| 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.
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 contains | Likely 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. |
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.
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.
- 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.
- 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.
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.
SSL Certificate Error — AWS RDS for SQL Server
All tables fail to connect with an error like:
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.
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:
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]')
migrationbridge/gui/db_schema.py —
ensure_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.
-
Close the GUI completely
Close the Endrias Bridge window and confirm no Python process is still running (check Task Manager).
-
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
-
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:
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.
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.
Temporal Tables: System-Versioning Not Re-Enabled
After a full migration the log shows lines like:
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.
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:
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:
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:
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:
- Near-max clamp (Change 44 → refined by Change 46): any
datetime.datetimeat9999-12-31 23:59:59withmicrosecond > 0is replaced withdatetime.datetime(9999, 12, 31, 23, 59, 59, 0).
Why zero, not 997000? ODBC Driver 17 withfast_executemany=Truerejects any non-zero fractional second at the year-9999 last-second boundary — including previously used values like 997000 µs — because thedatetime2tick encoder overflows for any sub-second value at that point. - MinValue clamp: year = 1 → replaced with
datetime.datetime(1753, 1, 1, 0, 0, 0).
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, 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:
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).
'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)
salesdata.CustomCodeCpt_HISTORY—Rate decimal(10,2)salesdata.AgeBasedRateDetail— wide decimal rate columnssalesdata.CustomFeeCptsalesdata.CustomProviderMatrixRateCpt
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:
Sequence [schema].[name]: created on target log line for
each newly created sequence and no action taken for sequences that already
exist.
TCP Drop Mid-Batch (Error 10054 / 08S01)
During a long-running migration you see a line like:
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.
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 15s (attempt 2/5)...
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
| Cause | Fix |
|---|---|
| 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
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 type | During load | Why |
|---|---|---|
| Non-clustered, non-unique | DISABLED → rebuilt after | Pure query indexes — safe to rebuild offline |
| Clustered (PK) | LEFT INTACT | Table data lives in the clustered index — disabling it drops all rows |
| Unique non-clustered | LEFT INTACT | Enforces uniqueness during INSERT — must remain active |
What you see in the log
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
| Method | Typical speed | 100 GB estimate |
|---|---|---|
| Python SQLAlchemy INSERT (old default) | 1–5 GB/hr | 20–100 hours |
| fast_executemany batch INSERT | 3–8 GB/hr | 12–33 hours |
| BCP OUT + BULK INSERT (current default) | 10–30 GB/hr | 3–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:
The migration continues normally — no data is lost or skipped.
What you see in the log when BCP runs
Troubleshooting bcp errors
| Symptom | Cause | Fix |
|---|---|---|
| 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
| Scenario | Recommended mode |
|---|---|
| Initial full copy of 10 GB – 1 TB | Full copy (with BCP + index disable) |
| Periodic delta sync (hourly / daily jobs) | CDC (single-pass) |
| Cut-over window — keep target live while traffic runs on source | CDC Stream |
| Real-time shadow copy for read-scale | CDC Stream |
How to start
- Enable CDC on source:
EXEC sys.sp_cdc_enable_db; - Enable CDC on each table:
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'YourTable', @role_name = NULL; - In Migration tab: select CDC Stream (real-time, SQL Server)
- Click Start Migration
- Monitor the log — cycles are logged as changes arrive
- When ready to cut over: click Stop Stream to drain and close
What you see in the log
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
| Requirement | Details | How 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
- 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();
- Enable CDC on each table to track
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'YourTable', @role_name = NULL; - Record the current LSN bookmark
SELECT sys.fn_cdc_get_max_lsn() AS start_lsn; -- save this value
- Run Full migration in Endrias Bridge — Migration tab → sync mode Full → Run Migration
- Validate row counts on source and target after copy completes
- Re-apply Schema Objects if needed (Schema Objects tab)
- 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
| Error | Cause | Fix |
|---|---|---|
| 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
| Check | How 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
| # | Action | Command / 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:
- Update app connection string back to source
- Restore write access on source:
GRANT INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [AppUser] - Investigate issue on target; fix; re-drain the stream; re-attempt cutover
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
| Requirement | CDC Stream | Scheduled 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
| Control | Description |
|---|---|
| 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 Stream | Discovers CDC-enabled source tables, builds engine pair, starts LSN tail loop in background thread. |
| Stop Stream | Signals graceful stop; current cycle drains before thread exits. State file is flushed. |
| LSN | Last processed Log Sequence Number (hex). Advances with every change cycle. |
| Rate | Rolling 60-second changes/second average. Turns red above 500/s (high change rate warning). |
| Total changes | Cumulative inserts + updates + deletes applied since stream started. |
| Schema drift alert | Red label — a tracked table's column set changed while streaming. Table is suspended until you stop, fix schema, and restart. |
Scheduled Sync — controls reference
| Control | Description |
|---|---|
| Sync every N minutes | Timer interval. Set to 1 for near-real-time on non-SQL-Server sources. Set to 60 for hourly batch sync. |
| Start Scheduled Sync | Reflects source and target metadata, finds common tables, runs sync_table_smart() per table on the timer. |
| Stop | Current cycle completes before thread exits. |
| Next run | Wall-clock time of next scheduled cycle. |
| Last run | Completion time of most recent cycle. |
Per-table dashboard — column meanings
| Column | Meaning |
|---|---|
| Table | Table name (schema.table if schema-qualified) |
| Mode | cdc = SQL Server CDC used; watermark = timestamp column used; no_sync = no watermark and no CDC — table skipped |
| Rows Synced | Cumulative rows applied (INSERTs + UPDATEs) this session |
| Last Sync | Timestamp of last change applied to this table |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
- In the Migration tab, find the Parallel streams per large table spinbox (default: 1)
- Set it to 4 for tables 400 M – 1 B rows; 8 for tables over 1 B rows
- Leave at 1 to use BCP (faster for smaller tables)
- 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
| Symptom | Cause | Fix |
|---|---|---|
| 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
Supported target type formats
| Format | Example |
|---|---|
| Simple name | xml = TEXT |
| Name with length | uniqueidentifier = VARCHAR(36) |
| DECIMAL with precision and scale | money = DECIMAL(19,4) |
| NVARCHAR | geography = NVARCHAR(4000) |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
Connection URI formats
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
Connection — Cosmos DB MongoDB API endpoint
Validated migration (July 2026)
| Source table | Documents written | Notes |
|---|---|---|
| HumanResources.Employee | 290 | OrganizationNode (hierarchyid) skipped — all other columns migrated |
| Person.Address | 19,614 | SpatialLocation (geography) skipped — all other columns migrated |
| Production.Product | 504 | Clean migration |
| Sales.Customer | 19,820 | Clean migration |
| Person.Person | 19,972 | Clean migration |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
Connecting
Supply AWS region + credentials. The connector accepts:
- Explicit
aws_access_key_id+aws_secret_access_key - Environment variables
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY - IAM instance role (no credentials needed on EC2/ECS)
endpoint_urlfor DynamoDB Local (testing)
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
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
- Status badge: idle / running / done / error
- Summary cards: rows done, elapsed time, throughput
- Overall progress bar
- Per-table mini progress bars with row count and status
- Color-coded log panel (auto-scrolls, errors red, warnings orange)
- Auto-refreshes every 2 seconds via JavaScript fetch
REST API (for CI/CD)
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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
Volume mounts
| Host path | Container path | Purpose |
|---|---|---|
./migrationbridge.conf | /app/migrationbridge.conf | Config (read-only) |
./migration_state/ | /app/state/ | Checkpoint JSON — survives container restart |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 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:
| Target | Impact | Fix |
|---|---|---|
| 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. |
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.
| Target | Impact | Fix |
|---|---|---|
| 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:
| Target | Fix |
|---|---|
| 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:
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").
| Target | Fix |
|---|---|
| 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:
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).
[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.| Target | Fix |
|---|---|
| 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
- Search the migration log for
[inventory] COLUMNSTORE,[inventory] FULLTEXT,[inventory] SPATIAL— these lines list every affected table - Search the migration log for
[idx] WARN: filtered index— lists every partial index that failed to create - For PostgreSQL targets with large tables: run
CLUSTERon high-traffic PK-range scan tables - Recreate columnstore / full-text / spatial indexes using the engine-appropriate DDL above
- Run your application's most critical queries with
EXPLAIN/EXPLAIN ANALYZEand 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
| Setup | Expected throughput | 52 GB estimate |
|---|---|---|
| Laptop over VPN, 5,000-row batches (old default) | 200–500 MB/hr | 10–26 hrs |
| Laptop over VPN, 100,000-row batches | 1–2 GB/hr | 2–4 hrs |
| Azure VM (same region as source), 100,000-row batches | 5–15 GB/hr | 20–60 min |
| Azure VM + AWS Direct Connect / VPC peering, 100,000-row batches | 15–30 GB/hr | 6–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:
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
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:
- Azure SQL → internet/VPN → your machine (source read)
- 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):
| Hop | Current (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:
WriteIOPS— should be <80% of provisioned IOPS ceilingWriteLatency— healthy target: <5 ms; >20 ms = IO bottleneckFreeStorageSpace— bulk inserts grow the transaction log; must not hit zeroCPUUtilization— >90% sustained during migration = instance is too small
Fix (migration window only — scale back after):
| Bottleneck | Fix |
|---|---|
| Write latency > 20 ms | Switch 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 fast | Pre-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:
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:
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):
| Table | Rows | Expected duration (40k rows/sec) |
|---|---|---|
| auditdata.Claims | 13,883,883 | ~5.8 min |
| analyticsdata.AggregateByEditFact | 13,972,964 | ~5.8 min |
| analyticsdata.AggregateLineFact | 26,923,039 | ~11.2 min |
| analyticsdata.AggregateHeaderDxCodeFact | 15,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.
- Confirm
migrationbridge.confhasdb_migration_chunk_size = 100000 - Run from an Azure VM or EC2 instance — not a laptop over VPN
- Switch RDS storage to gp3 and set IOPS to 6,000 for the migration window
- Verify the run is a Full migration (schema + data), not Resume
- Monitor CloudWatch
WriteLatency— target <5 ms - 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;
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
- Always run both SQL queries above before changing the batch size.
- Start at
100,000for narrow tables (confirmed baseline for enterprise workloads). - Watch the migration log for
Copying in batches of N rowsto confirm the new value loaded. - Monitor RDS free storage during a run — bulk inserts generate heavy transaction log.
- Restart the GUI after editing
migrationbridge.conf— the value is read only at startup. - Scale back the RDS instance class after migration to save cost.
- If you hit an OOM or timeout, drop back to
50,000— that is the proven stable floor.
DO NOT — common mistakes
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.
- Do not rely on estimated_row_bytes alone — it measures defined column width, not actual storage. Always check avg_actual_bytes_per_row too.
- Do not ignore tables with 1–5 rows showing huge avg bytes — that is page overhead, not a real risk signal.
- Do not increase batch size without checking RDS instance RAM first.
- Do not change
migrationbridge.confwhile a migration is running — the running session will not pick it up; it takes effect on the next GUI start. - Do not use batch sizes above
50,000on adb.t3.medium(4 GB RAM) RDS instance — the buffer pool will compete with the incoming INSERT batches. - Do not push to
500,000+in production — a mid-batch TCP drop loses the entire batch, forcing a full retry of 500k rows.
Endrias Bridge Confirmed Settings (Azure SQL → AWS RDS)
| Setting | Value | Notes |
|---|---|---|
| Batch size (tested stable) | 50,000 | Safe floor — confirmed no OOM, no timeout |
| Batch size (current production) | 100,000 | Running 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 item | Transaction log free space | Monitor 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.
| Resource | Where | What 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.large →
db.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. |
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
| Resource | 1 job alone | 5 jobs concurrent | Blocks? |
|---|---|---|---|
| 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
| Resource | Current (1 job) | Need for 5 concurrent |
|---|---|---|
| Instance class | 4 vCPU / 16 GB (db.m5.xlarge) | db.r5.2xlarge — 8 vCPU / 64 GB |
| IOPS | 3,000 (gp3 default) | 12,000 – 16,000 provisioned IOPS |
| Storage throughput | 125 MB/s | 500 MB/s |
| Storage size | — | 55 GB × 5 + 30% log overhead = 360 GB minimum |
| max server memory | 12,097 MB (75% of 16 GB) | 52,000 MB (80% of 64 GB) |
| Recovery model | FULL | SIMPLE on all target DBs during migration window |
Three ways to eliminate contention (choose one)
-
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
-
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
-
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 seen | Bottleneck | Fix |
|---|---|---|
WRITELOG | Log writer — IOPS | SIMPLE recovery + provision IOPS |
PAGEIOLATCH_EX | IOPS ceiling hit | Provision 9,000–12,000 IOPS in AWS Console |
RESOURCE_SEMAPHORE | Memory pressure | Reduce batch size to 50,000 or upgrade instance |
CXPACKET | Parallelism — normal under load | No action needed |
LCK_M_* | Lock contention | Should not appear — different DBs never block each other |
Decision matrix
| Scenario | Recommended approach |
|---|---|
| 5 DBs, test/dev, cost is priority | Stagger + SIMPLE recovery + keep 100k batch |
| 5 DBs, production, tight migration window | db.r5.2xlarge + 12,000 IOPS + SIMPLE recovery |
| 5 DBs, zero contention required | Separate RDS instance per DB (best option) |
| Migration machine is the bottleneck | Run 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 value | Meaning | Action |
|---|---|---|
| > 5,000 | Excellent — entire working set in RAM | None |
| 1,000 – 5,000 | Healthy — good buffer utilization | None |
| 300 – 1,000 | Under load — monitor closely | Watch trend |
| < 300 | Critical — severe page eviction | Reduce batch size or upgrade instance RAM |
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;
| Metric | Example observed value | What 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. |
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;
| Column | Example value | What to check |
|---|---|---|
physical_memory_mb | 16,111 MB | Total instance RAM — matches RDS instance class |
target_memory_mb | 12,097 MB | SQL Server memory ceiling — should be 75–80% of physical |
committed_memory_mb | 12,097 MB | At ceiling is normal and expected |
cpu_count | 4 | vCPUs available — upgrade instance class if CPU waits appear during concurrent runs |
max_workers_count | 512 | Default for 4-CPU. Adequate for migration workloads. |
scheduler_count | 4 | One scheduler per vCPU — correct |
Batch size memory safety at current instance specs
| Batch size | Memory per batch (widest table: 1,993 bytes/row) | % of 12 GB SQL Server budget | Safe? |
|---|---|---|---|
| 50,000 | ~95 MB | 0.8% | ✅ Yes |
| 100,000 | ~190 MB | 1.6% | ✅ Yes — confirmed production |
| 200,000 | ~380 MB | 3.1% | ✅ Yes — safe for this row width |
| 500,000 | ~950 MB | 7.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_ms | Meaning | Action |
|---|---|---|
| < 5 ms | Excellent — IOPS not a bottleneck | Proceed with concurrent migration |
| 5 – 20 ms | Moderate — some IO pressure | Consider provisioning IOPS before adding more concurrent jobs |
| 20 – 50 ms | High — IOPS ceiling being hit | Provision 9,000–12,000 IOPS in AWS Console before proceeding |
| > 50 ms | Critical — severe IO stall | Stop and provision IOPS immediately. Do not run concurrent migrations. |
How to provision IOPS on AWS RDS (gp3)
-
Open RDS Console
AWS Console → RDS → Databases → select your instance → Modify
-
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.
-
Apply immediately
Select "Apply immediately" — takes effect in 2–5 minutes with no downtime for gp3 IOPS changes. No reboot required.
-
Revert after migration
Drop IOPS back to 3,000 and throughput to 125 MB/s after all migrations complete to avoid unnecessary storage cost.
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 target | Use | Why |
|---|---|---|
| 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:
- Tables missing on target →
drop_allis a no-op,create_allcreates them fresh - Tables partially created (schema only, no data) → dropped and recreated clean
- Tables with existing data → dropped and recreated, then data is re-copied
- SSDT-deployed API objects (stored procs, functions, TVP types) → never touched by
drop_all(not TABLE objects in SQLAlchemy's metadata)
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.
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.
[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.
| Challenge | Scale (production environment) | How Endrias Bridge handles it |
|---|---|---|
| Interrupted migration restarting from row 0 | 1.07 B-row ClaimLineModifier | PK-range checkpoint — resumes from last committed PK |
| OOM crash on wide-LOB rows | 1.41 TB total DB, JSON/LOB tables ~13 KB/row avg | Auto LOB chunk sizing — caps each batch at 256 MB |
| INT PK overflow | 4.13 B-row EditorOverrideFilterItemValue | Pre-flight warning, recommends BIGINT on target |
| Azure SQL CDC truncates LOB > 64 KB | All nvarchar(max) tables | Auto-downgrade to watermark sync for LOB tables |
| Table too large for row copy | Large JSON/LOB tables in 1.41 TB+ DB | BACPAC seed via SqlPackage + CDC catch-up |
| Small tables blocked behind large tables | All reference/lookup tables | Two-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:
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.
Application schemas that legitimately produce TARGET ONLY entries
| Schema | Contains |
|---|---|
coreapi | Configuration API procs & TVPs |
priceapi | Fee schedule API procs, functions & TVPs |
customapi | Custom edit API procs & TVPs |
referenceapi | Age DX code API procs & TVPs |
catalogapi | Custom CPT/HCPCS API procs & TVPs |
mappingapi | Message map API procs & TVPs |
limitsapi | Frequency limit API procs & TVPs |
rulesapi | MUE API procs & TVPs |
dbo | Shared TVP aliases |
util | Utility 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:
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 see | What 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:
| Field | Correct example | Common mistakes |
|---|---|---|
| DB Type | SQL Server | — |
| Host/IP | localhost, 10.0.0.5, myserver.database.windows.net |
Pasting a full connection string; including tcp:, http://, or a port number here. |
| Port | 1433 | Leaving blank; including extra text like "1433 (default)". |
| Database | AppDb | Including the server name; trailing spaces from pasting. |
| Username | sa or your SQL login name | Including @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:
-
The exact error text
Copy the full error from the Migration tab log, including the
Connection: ...line. -
What "Test Connection" reported
On Step 5, click Test Connection for the affected server and note whether it says the port is reachable.
-
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.
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
| Error | Cause | Fix |
|---|---|---|
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
- Open
C:\MigrationTool\migrationbridge.confin Notepad - Add or find the
[chunk_overrides]section - Add a line:
TableName = 500(table name is case-insensitive) - Save and restart MigrationBridge — no service restart needed for this setting
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:
| Check | What it does |
|---|---|
| Host reachability | 2-second socket connect to host:1521 |
| Python driver | Tries import cx_Oracle, then import oracledb; shows version if found |
| Oracle Instant Client | Walks 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)
- Open a terminal (Command Prompt or PowerShell)
- Run:
pip install oracledb - 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.
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.
| Driver | Python 3.13? | Instant Client needed? | Oracle version | Status |
|---|---|---|---|---|
oracledb | ✓ Yes | No (thin mode) | 12.1+ | Recommended — use this |
cx_Oracle | ✗ No — build fails | Yes — 64-bit Basic pkg | 11g+ | Legacy only (Python ≤ 3.12) |
Common Oracle connection errors
| Error code | Meaning | Fix |
|---|---|---|
| DPI-1047 | Python driver found, Instant Client DLL not found on PATH | Add Instant Client folder to PATH, or switch to oracledb thin mode |
| ORA-01017 | Invalid username or password | Check credentials; Oracle passwords are case-sensitive (11g+) |
| ORA-12154 | TNS: could not resolve service name | Use Easy Connect format in the DB field: hostname:port/service_name |
| ORA-12541 | No listener at host:port | Run lsnrctl status; start listener if down; check firewall port 1521 |
| ORA-12170 | Connect timeout | Check 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:
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:
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:
confirms the grants were applied. All 90 FKs in AdventureWorks2019 apply cleanly.
If you still see ORA-00942 on FKs
| Check | Fix |
|---|---|
| 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:
or, if the table's only primary key is a hierarchyid column:
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)
- LOB types (
Text,LargeBinary) and unboundedString(length=None)are excluded from the positional ORDER BY. - When all PK columns are of an unmappable type (e.g.
hierarchyid-only PK onProduction.Document), verification falls back to count-only mode — no checksum comparison, no false mismatches. The log showssample_size=0to make this explicit.
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:
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 version | Path | What 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
EXECUTE ON DBMS_FLASHBACK_ARCHIVE(or DBA role)FLASHBACK ARCHIVEprivilege on the archive object itself- A tablespace with sufficient quota for the archive retention period
Notes on migrated data
- SQL Server
SysEnd = 9999-12-31 23:59:59.9999999(open-ended rows) is clamped to9999-12-31 23:59:59during data copy due to OracleTIMESTAMPprecision limits. This is correct behaviour — these rows represent currently-valid records. - For 23ai Path A,
ADD VERSIONING USE HISTORY TABLEwires the migrated history table in directly, so a singleAS OF PERIOD FOR SYSTEM_TIMEquery spans both pre- and post-migration history.
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:
This does not mean data is lost — the CDC log retains all changes. It means the replication lag is increasing.
What to check
| Bottleneck | How to diagnose | Fix |
|---|---|---|
| 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:
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
- Stop the CDC stream (click Stop Stream in the GUI)
- Apply the matching schema change to the target table (e.g.
ALTER TABLE ClaimHeader ADD AuditTimestamp DATETIME2) - Go to the Schema Objects tab → Re-apply Schema Objects to sync the target DDL
- Restart the CDC stream — it will re-snapshot column sets for all tables
- 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"
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 selected | SSL mode used | Works on |
|---|---|---|
| PostgreSQL (VM / On-Prem) | sslmode=prefer | Local, VM, container, dev laptop |
| AWS RDS for PostgreSQL | sslmode=require | RDS (SSL enforced) |
| Azure Database for PostgreSQL Flexible | sslmode=require | Azure (SSL enforced) |
| Amazon Aurora (PostgreSQL) | sslmode=require | Aurora (SSL enforced) |
| GCP Cloud SQL (PostgreSQL) | sslmode=require | GCP (SSL enforced) |
If you still see the error
- Confirm you selected PostgreSQL (VM / On-Prem) in the DB Type dropdown — not a cloud variant.
- If you must use SSL on your local server, enable it in
postgresql.conf:
Then restart PostgreSQL and placessl = onserver.crtandserver.keyin the data directory.
SQL Server: TCP timeout / server not found
08001 — TCP Provider: The wait operation timed outor:
A network-related or instance-specific error has occurred
SQL Server is unreachable from this machine. Work through this checklist in order:
- Correct host? — Check the Host/IP field. For named instances use
HOSTNAME\INSTANCENAME. For Azure SQL useserver.database.windows.net. - Service running? — SQL Server Configuration Manager → SQL Server Services → confirm status is Running.
- TCP/IP enabled? — SQL Server Configuration Manager → Protocols for MSSQLSERVER → TCP/IP → Enabled. Restart the service after changing.
- 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
- Remote connections allowed? — SSMS → Server Properties → Connections → Allow remote connections to this server must be checked.
- 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.
- 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
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 type | SSL behavior |
|---|---|
| MySQL (VM / On-Prem) | SSL disabled — plain connection |
| MariaDB | SSL disabled — plain connection |
| AWS RDS for MySQL | pymysql default — negotiates SSL automatically |
| Azure Database for MySQL Flexible | pymysql 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:
| Check | How to verify |
|---|---|
| Host / IP correct | Ping the host from this machine: ping hostname |
| Port reachable | Use Test Connection in the wizard — it probes the TCP port first |
| Firewall allows your IP | Your public IP is shown in the connection error dialog — add it to the cloud firewall / NSG / Security Group |
| Correct DB type selected | On-prem types (VM / On-Prem) use permissive SSL; cloud types use strict SSL. Mismatch causes SSL errors. |
| ODBC Driver 17 installed | Required for SQL Server connections. Download: Microsoft ODBC Driver 17 for SQL Server |
| psycopg2 installed | Required for PostgreSQL. Included in the Endrias Bridge installer. |
| pymysql installed | Required for MySQL / MariaDB / Aurora MySQL. Included in the installer. |
| Database exists | SQL Server error 4060 = database not found. PostgreSQL: FATAL: database "x" does not exist. |
| User has login permission | SQL 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):
Error: "Change Tracking is not enabled on table [schema].[table]"
Cause: CT enabled at DB level but not on the individual table.
Fix:
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:
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.