SQL Server → Azure SQL Database
Step-by-step procedure for migrating an on-premises SQL Server database to Azure SQL Database (General Purpose or Business Critical tier) using Endrias Bridge.
Overview & Scope
This SOP covers a one-time full copy (schema + data) of a SQL Server on-premises or VM database into an existing Azure SQL Database, using the Endrias Bridge Assessment and Migration tabs. Because source and target are the same engine family (T-SQL), most types map 1:1 — see the Type Conversions table for the few exceptions (deprecated types, FILESTREAM, temporal SysEnd clamping).
Prerequisites — Source (SQL Server)
| Requirement | Notes |
|---|---|
SQL login with db_datareader + VIEW SERVER STATE | Minimum |
db_owner recommended for CDC stream mode | Recommended |
| ODBC Driver 17 for SQL Server installed on migration host | Required |
| TCP 1433 open from migration host to source server | Required |
| SQL Server version 2016+ (Compatibility level 130+) | Required |
-- Verify source SQL Server version and compatibility level
SELECT @@VERSION;
SELECT name, compatibility_level
FROM sys.databases
WHERE name = 'EndriasBridge';
-- Grant minimum permissions on source
USE EndriasBridge;
CREATE USER MigrationUser FOR LOGIN MigrationUser;
ALTER ROLE db_datareader ADD MEMBER MigrationUser;
GRANT VIEW SERVER STATE TO MigrationUser;
Prerequisites — Target (Azure SQL Database)
Endrias Bridge does not create the Azure SQL Database — it must exist and be accessible before Step 1. Complete the following in the Azure Portal before connecting:
1. Add migration host firewall rule:
Navigate to Azure Portal → SQL Server (logical server) → Networking → Firewall rules. Add the public IP of your migration host as an allowed inbound rule.
2. Create contained database user on target:
-- Run on Azure SQL Database (EndriasBridgeTarget) via SSMS or Azure Portal Query Editor
CREATE USER MigrationUser WITH PASSWORD = 'your-password';
ALTER ROLE db_owner ADD MEMBER MigrationUser;
3. Verify tier capacity:
| Tier | Max Size | Migration Recommendation |
|---|---|---|
| Basic | 2 GB | Not recommended for large migrations |
| Standard S3 | 100 GB | Minimum for production migration throughput |
| General Purpose | Up to 4 TB | Recommended |
| Business Critical | Up to 4 TB | Recommended for large, time-sensitive migrations |
TCP 10054 / forcibly closed errors and automatically retries once after disposing the connection pool — no manual intervention is needed for this known Azure behavior.Configure Connections
Open Endrias Bridge and navigate to Step 5 — DB Migration. Fill in both connection blocks as shown below. For the Azure SQL target, use the fully qualified server hostname from the Azure Portal (yourserver.database.windows.net) and port 1433.
SOURCE DATABASE
TARGET DATABASE
Run Assessment
Switch to the Assessment tab and click Run Assessment. The tool connects to the source only at this stage. Since source and target are the same engine family, most objects will show as compatible — look for deprecated type warnings (NTEXT, IMAGE, TEXT) and any FILESTREAM columns.
SOURCE OBJECTS
Users 45,200 rows
Products 8,100 rows
Orders 2,400,000 rows
CustomCodeDetail 1,240 rows
… (54 more)
COMPATIBILITY ISSUES
• Documents.FileData: IMAGE → VARBINARY(MAX) (deprecated)
• CustomCodeDetail: SysEnd temporal boundary → clamp warning
• (Most standard T-SQL types: 1:1 mapping)
• … (full list per scanned table)
NTEXT, IMAGE, TEXT) that Endrias Bridge upgrades automatically, FILESTREAM columns (migrated as VARBINARY(MAX) — files not copied), and temporal SysEnd clamping warnings (informational).Select Tables
Switch to the Migration tab. Click Load Tables from Source to populate the table list. For a SQL Server → Azure SQL migration, all tables are generally safe to include — review the list for any tables that contain FILESTREAM data or rely on cross-database joins that will not work post-migration.
Start Migration & Monitor
Click Start Migration. Watch the Migration Log update live. The log will show type upgrade notes for deprecated types and any temporal SysEnd clamping warnings — these warnings are informational and do not indicate data loss.
SysEnd = 9999-12-31 to 9998-12-31. This is a known driver limitation, not a data integrity issue.TCP 10054 or forcibly closed error mid-migration, Endrias Bridge will automatically retry after disposing the connection pool. This is expected behavior for Azure SQL idle connection drops — the migration will resume and complete without manual intervention.Verify & Export Report
- Click Verify Data Integrity — compares source vs. target row counts for every migrated table and logs any mismatches.
- Click Export Report (enabled once migration finishes) to save an HTML/text summary of what was created, copied, and any warnings.
- Spot-check key tables on the Azure SQL target using SSMS or Azure Portal Query Editor.
-- Run on Azure SQL target after migration to spot-check row counts
SELECT t.name AS TableName, p.rows AS RowCount
FROM sys.tables t
INNER JOIN sys.partitions p ON t.object_id = p.object_id
WHERE p.index_id IN (0, 1)
ORDER BY t.name;
Post-Migration
Azure SQL Database (PaaS) does not support all SQL Server on-prem features. Review the following items after migration completes.
1. Recreate SQL Agent jobs as Azure Elastic Jobs:
SQL Server Agent is not available on Azure SQL Database (PaaS). Export all job definitions from the source using the Schema Objects tab, then recreate them as Azure Elastic Jobs via the Azure Portal or T-SQL elastic jobs API.
2. Review FILESTREAM / FILETABLE columns:
-- Identify FILESTREAM columns on target (will be VARBINARY(MAX))
SELECT OBJECT_NAME(c.object_id) AS TableName, c.name AS ColumnName, t.name AS TypeName
FROM sys.columns c
JOIN sys.types t ON c.user_type_id = t.user_type_id
WHERE t.name = 'varbinary' AND c.max_length = -1
ORDER BY TableName, ColumnName;
FILESTREAM files were not migrated — binary column data is present in VARBINARY(MAX) but the file-system projection is gone. Migrate file payloads separately to Azure Blob Storage and update application connection strings accordingly.
3. Validate linked server references:
Linked servers are not available on Azure SQL Database. Replace with OPENDATASOURCE, elastic queries (external tables), or Azure Data Factory pipelines for any cross-database or cross-server references.
sp_configure 'clr enabled' option is not user-accessible on PaaS — submit an Azure support request if CLR is required for your workload.OtherDB.dbo.TableName) will fail on Azure SQL. Replace these with elastic queries (external data sources and external tables) or consolidate schemas into a single database.Live Migration — Full-Load-First Sequence
| Phase | Action | Notes |
|---|---|---|
| 1 | Enable CDC on source (one-time setup) | Must be done before the full load starts. Tool records the starting LSN anchor at this point. |
| 2 | Click Start Migration → Full Load | MigrationBridge saves sys.fn_cdc_get_max_lsn() before copying row 1. Source stays fully live throughout. |
| 3 | Full load completes | Watch Migration Log — all tables show row counts. Do not start the stream until this step finishes. |
| 4 | Start CDC Stream | Tool replays all changes that accumulated on the transaction log during the full load, from the saved LSN forward. Catches up to real-time, then enters sub-second tail mode. No changes are missed. |
| 5 | Monitor lag < 1 second | Watch Replication tab or Migration Log. When lag is consistently under 1 second for 5 minutes, you are in steady-state tail mode. |
sys.fn_cdc_get_max_lsn() and saves that position before copying a single row.
If the full load takes 90 minutes and thousands of rows change on the source during that time,
the stream catch-up phase after the load will replay every one of those changes in order —
nothing is lost, regardless of how long the full load took.
Live Migration — Source Pre-flight Checks
Run all four blocks on the VM source before enabling CDC. All checks must pass.
USE YourSourceDB;
GO
-- 1. SQL Agent running? (CDC capture job requires it)
SELECT servicename, status_desc
FROM sys.dm_server_services
WHERE servicename LIKE 'SQL Server Agent%';
-- Expected: Running
-- 2. Recovery model FULL?
SELECT name, recovery_model_desc
FROM sys.databases WHERE name = DB_NAME();
-- Expected: FULL
-- 3. Containment? (PARTIAL blocks CDC -- see note below)
SELECT name, containment_desc
FROM sys.databases WHERE name = DB_NAME();
-- Expected: NONE
-- 4. CDC already enabled?
SELECT name, is_cdc_enabled
FROM sys.databases WHERE name = DB_NAME();
-- Expected: 0 (will enable next) or 1 (already done)
-- 5. Permission check
SELECT IS_MEMBER('db_owner') AS is_db_owner, SUSER_SNAME() AS login;
SELECT name FROM sys.database_principals WHERE authentication_type_desc = 'DATABASE'),
(2) Verify zero rows remain before continuing,
(3) Run ALTER DATABASE YourSourceDB SET CONTAINMENT = NONE from master
(single-user mode required),
(4) Re-run sp_cdc_enable_db.
The ALTER fails silently if even one contained user still exists.
Round 1 — CDC Stream Setup (VM Source)
Enable CDC on the database
USE YourSourceDB;
GO
EXEC sys.sp_cdc_enable_db;
GO
GRANT SELECT ON SCHEMA::cdc TO [Migration];
GO
-- Set 30-day retention immediately (default is 3 days)
EXEC sys.sp_cdc_change_job @job_type='cleanup', @retention=43200; -- 43200 min = 30 days
GO
-- Verify CDC on and jobs created
SELECT name, is_cdc_enabled FROM sys.databases WHERE name = DB_NAME();
SELECT name, enabled FROM msdb.dbo.sysjobs WHERE name LIKE 'cdc.YourSourceDB%';
-- Expected: 2 rows (capture + cleanup)
EXEC sys.sp_cdc_add_job @job_type = N'capture' immediately after
sp_cdc_enable_db, because Azure SQL does not auto-create the capture job.
On a VM source with SQL Agent running this step is automatic — skip it.
Target (Azure SQL): CDC does NOT need to be enabled on the target. MigrationBridge applies changes as plain INSERT / UPDATE / DELETE. The target only needs
db_owner.
Enable CDC on the test table
USE YourSourceDB;
GO
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'YourTable',
@role_name = NULL,
@supports_net_changes = 1;
GO
-- Generate enable statements for ALL tables at once
SELECT
'EXEC sys.sp_cdc_enable_table @source_schema=N''' + s.name + ''','
+ '@source_name=N''' + t.name + ''','
+ '@role_name=NULL,@supports_net_changes=1;' AS stmt
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
LEFT JOIN cdc.change_tables ct ON ct.source_object_id = t.object_id
WHERE s.name NOT IN ('cdc','sys') AND ct.capture_instance IS NULL
ORDER BY s.name, t.name;
-- Copy the output and execute the generated statements
Configure MigrationBridge and run
- Source: SQL Server → VM hostname/IP,
YourSourceDB,MigrationUser - Target: Azure SQL Database →
yourserver.database.windows.net, port 1433 - Migrate tab → Sync Mode → CDC Stream (real-time, SQL Server)
- Select tables → click Start Migration — full load runs, LSN anchor saved
- Wait for full load to complete — watch Migration Log until all tables show row counts
- Click Start CDC Stream — catch-up begins, then enters tail mode
Test and validate
-- Run on VM source
USE YourSourceDB;
UPDATE dbo.YourTable SET SomeColumn = 'CDC_TEST_1' WHERE ID = 1;
-- Run on Azure SQL target within ~1 second
SELECT SomeColumn FROM dbo.YourTable WHERE ID = 1;
-- Expected: 'CDC_TEST_1'
-- Also test UPDATE and DELETE
UPDATE dbo.YourTable SET SomeColumn = 'CDC_UPDATE' WHERE ID = 1;
DELETE FROM dbo.YourTable WHERE ID = 999; -- use a real dispensable row
Round 2 — CT Sync Setup (Change Tracking)
Change Tracking requires no SQL Agent and is simpler to configure. Use this if CDC is unavailable or as a comparison test.
Enable Change Tracking on the VM source
USE YourSourceDB;
GO
-- Database level — 30-day retention
ALTER DATABASE YourSourceDB
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 30 DAYS, AUTO_CLEANUP = ON);
GO
-- Per table
ALTER TABLE dbo.YourTable
ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);
GO
-- Generate statements for all tables
SELECT 'ALTER TABLE ' + s.name + '.' + t.name
+ ' ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED=ON);'
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
WHERE s.name NOT IN ('cdc','sys')
ORDER BY s.name, t.name;
-- Verify
SELECT db_name, retention_period, retention_period_units_desc, is_auto_cleanup_on
FROM sys.change_tracking_databases WHERE database_id = DB_ID();
SELECT t.name, ct.is_track_columns_updated_on
FROM sys.change_tracking_tables ct
JOIN sys.tables t ON ct.object_id = t.object_id;
Configure MigrationBridge and run
- Same Source + Target connections as Round 1
- Sync Mode → CT Sync (watermark)
- Click Start Migration → full load runs
- After full load → click Start CT Sync stream
Test all three DML operations
-- Run on VM source
UPDATE dbo.YourTable SET SomeColumn = 'CT_UPDATE' WHERE ID = 1;
DELETE FROM dbo.YourTable WHERE ID = 999;
INSERT INTO dbo.YourTable (ID, SomeColumn) VALUES (9999, 'CT_INSERT');
-- Verify each on Azure SQL target within ~1-5 seconds
CDC Stream vs CT Sync — Comparison
| Feature | CDC Stream | CT Sync |
|---|---|---|
| Replication latency | ~250ms (sub-second) | ~1–5 seconds (poll interval) |
| DELETE capture | Yes — native from transaction log | Yes — tombstone pattern |
| SQL Agent dependency | Yes — capture job requires Agent running | No — no Agent needed |
| Recovery model requirement | FULL required | Any recovery model |
| Setup complexity | More steps (sp_cdc_enable_db + per table) | Simpler (ALTER DATABASE + ALTER TABLE) |
| Azure SQL source | Requires sp_cdc_add_job manually | Native — recommended for Azure SQL sources |
| Best for | Sub-second RPO, high-churn tables, compliance | Simpler setup, lower-churn DBs, Azure SQL sources |
| Rollback / disable | sp_cdc_disable_table → sp_cdc_disable_db | ALTER TABLE DISABLE CHANGE_TRACKING → ALTER DATABASE SET CHANGE_TRACKING = OFF |
UI Busy / Not Responding During Migration
What you will see
| Symptom | Cause | Action |
|---|---|---|
| Title bar shows "(Not Responding)" | UI thread is busy flushing log lines while a large table is being copied at high throughput | Wait — do not close the window. The window will become responsive again when the batch finishes. This typically lasts 5–30 seconds on large tables. |
| Progress bar frozen | Same as above — UI update is queued while background thread is running | Wait. Check Migration Log for last completed table — if rows are incrementing in the log, migration is progressing normally. |
| Migration Log stops updating | Log panel is waiting for the UI thread to flush | Normal during large table copy. When the table finishes, the log will catch up and display all rows at once. |
| CDC Stream shows no new events for 10+ seconds | Source has no changes, or the polling interval is idle | Make a test change on the source (UPDATE one row). If it appears on the target within 1–2 seconds, the stream is healthy. |
| Window freezes and stays frozen > 5 minutes | Possible genuine hang — distinct from normal busy state | Check Task Manager → CPU/Memory for MigrationBridge process. If CPU is 0% and memory is not growing, check migration log file at C:\MigrationT\migration_logs\ for the last logged entry to diagnose. |
How to tell if migration is still running
- Open Task Manager → find
python.exeorMigrationBridge.exe→ confirm CPU % is above 0 and memory is active. If so, the migration is running — the UI is just catching up. - Open the latest log file directly:
C:\MigrationT\migration_logs\— the file updates in real time even when the UI is frozen. - For CDC Stream: check the source with
SELECT sys.fn_cdc_get_max_lsn() AS source_lsnand compare it to the last LSN shown in the Migration Log. If they are close, the stream is healthy.
Best practices to reduce UI freeze frequency
- Run from source rather than the installed binary:
py C:\MigrationT\Migrationtool\launch_gui.pyw— this uses the latest version which has reduced UI flush frequency on large tables. - Avoid resizing or interacting with the window during high-throughput table copy — UI interactions queue additional render work.
- For very large databases (> 5 million rows), consider running the migration overnight and checking the log file the next morning rather than watching the UI live.
- For CDC Stream: once the full load is complete and streaming has started, the UI is mostly idle (polling every 250ms) and will not freeze under normal change volumes.
Get-Content "C:\MigrationT\migration_logs\migration_$(Get-Date -f yyyyMMdd)*.log" -Wait -Tail 20
This shows live log output without touching MigrationBridge at all.
Known Issues
Temporal table SysEnd clamping: ODBC Driver 17 cannot write year-9999 datetime boundaries. Endrias Bridge automatically clamps SysEnd = 9999-12-31 to 9998-12-31. Log lines showing WARNING: SysEnd clamped are informational — data lands correctly on target.
FILESTREAM / FILETABLE: Not supported on Azure SQL Database. Columns are migrated as VARBINARY(MAX). Files stored in FILESTREAM must be migrated separately to Azure Blob Storage.
SQL Server Agent jobs: Not available on Azure SQL (PaaS). Recreate as Azure Elastic Jobs post-migration.
Linked servers: Not available on Azure SQL. Replace with OPENDATASOURCE, external tables, or Azure Data Factory pipelines.
CLR assemblies: Require explicit enablement on Azure SQL (sp_configure 'clr enabled' is not available — submit a support request for CLR).
Cross-database queries: Azure SQL does not support 3-part cross-database names. Use elastic queries instead.
TCP 10054 (idle connection drop): Azure SQL drops idle pooled connections after ~30 minutes. Endrias Bridge detects 10054/forcibly closed errors and automatically retries once after disposing the connection pool.
| Issue | Impact | Resolution |
|---|---|---|
| Temporal SysEnd clamping | SysEnd 9999-12-31 → 9998-12-31 | Informational warning — data is correct on target |
| FILESTREAM / FILETABLE | Files not migrated; column becomes VARBINARY(MAX) | Migrate files separately to Azure Blob Storage |
| SQL Agent jobs | Not available on Azure SQL PaaS | Recreate as Azure Elastic Jobs |
| Linked servers | Not available on Azure SQL PaaS | Use OPENDATASOURCE, elastic tables, or ADF |
| CLR assemblies | Require PaaS support ticket to enable | Submit Azure support request if CLR required |
| Cross-database 3-part names | Queries fail on Azure SQL | Use elastic queries or consolidate databases |
| TCP 10054 idle drop | Mid-migration connection reset | Auto-retry built in — no manual action needed |
Type Conversions Applied (SQL Server → Azure SQL)
| Source Type | Target Type | Note |
|---|---|---|
| All standard T-SQL types | Same type | Same engine family — 1:1 mapping |
| NTEXT | NVARCHAR(MAX) | NTEXT is deprecated; Azure SQL still accepts it but Endrias Bridge upgrades |
| IMAGE | VARBINARY(MAX) | IMAGE is deprecated |
| TEXT | VARCHAR(MAX) | TEXT is deprecated |
| FILESTREAM VARBINARY | VARBINARY(MAX) | FILESTREAM attribute removed — files not migrated |
| hierarchyid / geography | TEXT or native | Migrated natively if target supports; fallback to TEXT via .ToString() |
| Temporal SysEnd=9999-12-31 | 9998-12-31 | ODBC Driver 17 clamping — informational warning, not data loss |