Standard Operating Procedure

SQL Server → MongoDB

Cross-engine migration from SQL Server (relational) to MongoDB (document store). MigrationBridge converts each row to a BSON document, maps SQL Server types to MongoDB types, and handles hierarchyid/geography gracefully.

Source: SQL Server 2016+  ·  Target: MongoDB 3.6+ (on-prem, Atlas, Cosmos DB Mongo API)  ·  Tool: MigrationBridge v1.3.0

📄 Overview

MongoDB is a document store — there is no fixed schema, no tables, and no rows. MigrationBridge bridges the conceptual gap by applying a deterministic mapping from SQL Server's relational model to MongoDB's document model. Collections are created automatically on the target; no DDL is required before running the migration.

Conceptual Mapping

SQL Server Concept MongoDB Equivalent Notes
DatabaseDatabaseSame name by default; configurable
Schema + TableCollectionNamed schema_tablename (e.g., HumanResources_Employee)
RowDocument (BSON)One row becomes one JSON/BSON object
ColumnFieldColumn name becomes field key
Primary Key_id (auto-generated)Original PK preserved as a separate field; MongoDB generates its own _id
NULL valueField omittedBy default MigrationBridge omits NULL fields; configurable to emit null
Foreign KeyNo enforcementReferences become plain field values; referential integrity is application-level
IndexIndexNot auto-migrated; must be created post-migration (see Step 6)
ViewNot migratedViews are read-only; exclude or pre-materialize
No schema enforcement on target. MongoDB does not enforce column types or required fields. MigrationBridge applies type coercion at insert time; schema validation rules (if desired) must be added as MongoDB JSON Schema validators after migration.
Collections auto-created. You do not need to create collections or databases on the target in advance. MigrationBridge issues insertMany operations; MongoDB creates the collection on first write.

Validated Configuration

This SOP was validated against AdventureWorks2019 — 5 tables, 60,200 documents migrated successfully. Two columns containing hierarchyid and geography were skipped with warnings; all other columns migrated cleanly.

TableCollectionRows / DocsNotes
HumanResources.EmployeeHumanResources_Employee290OrganizationNode (hierarchyid) SKIPPED
Person.AddressPerson_Address19,614SpatialLocation (geography) SKIPPED
Sales.SalesOrderHeaderSales_SalesOrderHeader31,465Clean
Production.ProductProduction_Product504Clean
Sales.SalesOrderDetailSales_SalesOrderDetail8,327Clean

🔌 Prerequisites — SQL Server (Source)

Required Permissions

The MigrationBridge service account requires db_datareader on the source database. No write permissions are needed on the source. Use the principle of least privilege.

-- Create a dedicated migration login (if not already present)
USE [master];
CREATE LOGIN [mb_reader] WITH PASSWORD = N'Str0ngP@ssw0rd!';

-- Add to source database as db_datareader
USE [AdventureWorks2019];
CREATE USER [mb_reader] FOR LOGIN [mb_reader];
EXEC sp_addrolemember N'db_datareader', N'mb_reader';

Required Driver

Pre-Flight Checks

Run the following queries on the source SQL Server before starting the migration to confirm connectivity and baseline row counts.

-- 1. Confirm SQL Server version (2016 SP1+ required for full JSON support)
SELECT @@VERSION;

-- 2. Confirm the migration login has datareader access
SELECT
    dp.name        AS [DatabasePrincipal],
    r.name         AS [RoleMembership],
    dp.type_desc   AS [PrincipalType]
FROM  sys.database_role_members  drm
JOIN  sys.database_principals    r   ON drm.role_principal_id  = r.principal_id
JOIN  sys.database_principals    dp  ON drm.member_principal_id = dp.principal_id
WHERE dp.name = N'mb_reader';

-- 3. Row count baseline for target tables
SELECT
    SCHEMA_NAME(t.schema_id)                    AS SchemaName,
    t.name                                       AS TableName,
    p.rows                                       AS ApproxRowCount
FROM  sys.tables   t
JOIN  sys.indexes  i  ON t.object_id = i.object_id  AND i.index_id <= 1
JOIN  sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
WHERE SCHEMA_NAME(t.schema_id) IN (N'HumanResources',N'Person',N'Sales',N'Production')
ORDER BY SchemaName, TableName;

-- 4. Identify any unsupported column types that will be skipped
SELECT
    SCHEMA_NAME(t.schema_id)  AS SchemaName,
    t.name                     AS TableName,
    c.name                     AS ColumnName,
    tp.name                    AS DataType
FROM  sys.columns c
JOIN  sys.tables  t   ON c.object_id = t.object_id
JOIN  sys.types   tp  ON c.user_type_id = tp.user_type_id
WHERE tp.name IN (N'hierarchyid', N'geography', N'geometry')
ORDER BY SchemaName, TableName, ColumnName;
TCP/IP must be enabled on the SQL Server instance. In SQL Server Configuration Manager, confirm the TCP/IP protocol is enabled and note the port (default 1433). Named instances require the SQL Server Browser service.

📄 Prerequisites — MongoDB (Target)

Python Driver

MigrationBridge uses pymongo to communicate with MongoDB. Install it in the MigrationBridge virtual environment:

pip install pymongo[srv]

The [srv] extra is required for Atlas mongodb+srv:// connection strings.

Connection String Formats

Deployment TypeConnection String Format
Local / On-Prem mongodb://localhost:27017
Local with auth mongodb://username:password@localhost:27017/?authSource=admin
MongoDB Atlas mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority
Cosmos DB Mongo API mongodb://accountname:primarykey@accountname.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@accountname@
Replica Set mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=rs0&authSource=admin
Cosmos DB Mongo API: Set retrywrites=false — Cosmos does not support retryable writes in all configurations. The ssl=true parameter is mandatory.

Create Target User

Create a dedicated migration user with readWrite on the target database. Connect to MongoDB as an admin user and run:

use admin
db.createUser({
  user: "mb_writer",
  pwd: "Str0ngP@ssw0rd!",
  roles: [
    { role: "readWrite", db: "AdventureWorks2019" }
  ]
})

Test Connectivity with mongosh

Before configuring MigrationBridge, confirm the connection string works from the migration host:

# Local instance
mongosh "mongodb://mb_writer:Str0ngP@ssw0rd!@localhost:27017/AdventureWorks2019?authSource=admin"

# Atlas
mongosh "mongodb+srv://mb_writer:Str0ngP@ssw0rd!@cluster0.xxxxx.mongodb.net/AdventureWorks2019"

# Cosmos DB Mongo API
mongosh "mongodb://accountname:primarykey@accountname.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false"

After connecting, run a quick sanity check:

db.runCommand({ ping: 1 })
// Expected: { ok: 1 }
Atlas IP allowlist. If connecting to MongoDB Atlas, ensure the IP address of the MigrationBridge host is added to the Atlas Network Access allowlist. For on-prem-to-Atlas migrations through NAT, add the NAT gateway's egress IP.
1

Configure Connections

Open MigrationBridge and navigate to Connections. Configure the source (SQL Server) and target (MongoDB) connection profiles. Both profiles are saved locally and encrypted at rest.

  MigrationBridge v1.3.0 — Connection Setup
Source
Target
Test

Source — SQL Server

SQL Server
SQLPROD01
1433
AdventureWorks2019
SQL Login
mb_reader
••••••••••••

Target — MongoDB

MongoDB
URI / Connection String
AdventureWorks2019
mongodb+srv://mb_writer:••••@cluster0.xxxxx.mongodb.net/
Auto (from URI)
For MongoDB targets, the Password field in legacy connection mode is not used. Supply the full URI (including credentials) in the URI field. MigrationBridge does not split the Mongo URI into host/user/password components.
Test Source Connection Test Target Connection Save Profiles
[10:14:02] Source connection OK — SQL Server 2019 (15.0.4355.3) AdventureWorks2019
[10:14:04] Target connection OK — MongoDB 6.0.8 (Atlas, cluster0.xxxxx.mongodb.net)
Cosmos DB Mongo API — use URI mode only. The host/port fields do not correctly encode the required SSL and replica set parameters for Cosmos. Always paste the full Cosmos connection string into the URI field.
2

Assessment

Click Run Assessment after saving connection profiles. MigrationBridge scans the source catalog and produces a readiness report before any data is moved.

What the Assessment Checks

Unsupported Type Handling

SQL Server TypeAssessment ResultMigration Behavior
hierarchyidSKIP — WarningColumn excluded; warning logged per row
geographySKIP — WarningColumn excluded; warning logged per row
geometrySKIP — WarningColumn excluded; warning logged per row
TEXT / NTEXTUPGRADE — InfoMigrated as String (UTF-8)
IMAGEUPGRADE — InfoMigrated as BinData subtype 0
sql_variantSKIP — WarningColumn excluded; manual handling required
XMLUPGRADE — InfoMigrated as String (raw XML text)
hierarchyid and geography are not errors. Skipped columns do not abort the migration — the row is still inserted without those fields. A warning is logged to the migration log for post-migration review. The skipped column count is shown in the final summary.

NULL Field Handling

SQL Server NULL values have two treatment modes in MigrationBridge:

Omit field (default) — The field is absent from the document. db.col.findOne({field:{$exists:false}}) matches these rows.
Emit null — The field is present with a BSON null value. db.col.findOne({field:null}) matches.

Set the preferred mode under Settings > MongoDB > Null Field Behavior before running migration. The default (omit) produces leaner documents and is idiomatic MongoDB.

3

Select Tables

After assessment, MigrationBridge displays the full table list. Use the checkboxes to include or exclude tables.

  MigrationBridge — Table Selection
Table Target Collection Est. Rows Status
HumanResources.Employee
HumanResources_Employee 290 1 SKIP
Person.Address
Person_Address 19,614 1 SKIP
Sales.SalesOrderHeader
Sales_SalesOrderHeader 31,465 Clean
Production.Product
Production_Product 504 Clean
Sales.SalesOrderDetail
Sales_SalesOrderDetail 8,327 Clean
Exclude System Tables Select All Clear All

Collection Naming Convention

MigrationBridge creates MongoDB collections named SchemaName_TableName by default. Examples:

To override collection names, use Settings > MongoDB > Collection Name Template. Available tokens: {schema}, {table}.

Exclude system tables. Always click Exclude System Tables before proceeding. MigrationBridge will filter sys.*, INFORMATION_SCHEMA.*, and internal SQL Server catalog tables automatically when this option is active.
Tables flagged SKIP in the Status column have at least one column that will be omitted (hierarchyid, geography, etc.). The table is still migrated — only the flagged column(s) are dropped per document. Review the warning count and confirm this is acceptable before continuing.
4

Run Migration

Click Start Migration. MigrationBridge streams rows from SQL Server in configurable batch sizes (default 1,000 rows per batch) and issues insertMany calls to MongoDB. Progress is shown per table.

  MigrationBridge — Migration Log
Start Migration Pause Export Log
[10:17:00] MigrationBridge v1.3.0 — SQL Server → MongoDB
[10:17:00] Source: SQLPROD01 / AdventureWorks2019
[10:17:00] Target: cluster0.xxxxx.mongodb.net / AdventureWorks2019
[10:17:00] Tables selected: 5 | Batch size: 1000
[10:17:01] ─────────────────────────────────────────────────────
[10:17:01] [1/5] Starting: HumanResources.Employee → HumanResources_Employee
[10:17:01] WARN Column [OrganizationNode] type=hierarchyid — SKIPPED (not supported in MongoDB)
[10:17:01] Type conversions: BusinessEntityID→Int32, NationalIDNumber→String, LoginID→String,
[10:17:01] JobTitle→String, BirthDate→Date(ISODate), MaritalStatus→String,
[10:17:01] Gender→String, HireDate→Date(ISODate), SalariedFlag→Boolean,
[10:17:01] VacationHours→Int32, SickLeaveHours→Int32, CurrentFlag→Boolean,
[10:17:01] rowguid→String(UUID), ModifiedDate→Date(ISODate)
[10:17:02] OK HumanResources_Employee — 290 documents inserted (1 column skipped per doc)
[10:17:02] ─────────────────────────────────────────────────────
[10:17:02] [2/5] Starting: Person.Address → Person_Address
[10:17:02] WARN Column [SpatialLocation] type=geography — SKIPPED (not supported in MongoDB)
[10:17:02] Type conversions: AddressID→Int32, AddressLine1→String, AddressLine2→String,
[10:17:02] City→String, StateProvinceID→Int32, PostalCode→String,
[10:17:02] rowguid→String(UUID), ModifiedDate→Date(ISODate)
[10:17:05] Batch 1/20: 1000 docs Batch 2/20: 1000 docs ... Batch 20/20: 614 docs
[10:17:09] OK Person_Address — 19,614 documents inserted (1 column skipped per doc)
[10:17:09] ─────────────────────────────────────────────────────
[10:17:09] [3/5] Starting: Sales.SalesOrderHeader → Sales_SalesOrderHeader
[10:17:09] Type conversions: SalesOrderID→Int32, RevisionNumber→Int32, OrderDate→Date,
[10:17:09] DueDate→Date, ShipDate→Date, Status→Int32, OnlineOrderFlag→Boolean,
[10:17:09] SalesOrderNumber→String, PurchaseOrderNumber→String, AccountNumber→String,
[10:17:09] CustomerID→Int32, ShipToAddressID→Int32, BillToAddressID→Int32,
[10:17:09] SubTotal→Double, TaxAmt→Double, Freight→Double, TotalDue→Double,
[10:17:09] rowguid→String(UUID), ModifiedDate→Date(ISODate)
[10:17:38] Batch 1/32: 1000 docs ... Batch 32/32: 465 docs
[10:17:41] OK Sales_SalesOrderHeader — 31,465 documents inserted
[10:17:41] ─────────────────────────────────────────────────────
[10:17:41] [4/5] Starting: Production.Product → Production_Product
[10:17:41] Type conversions: ProductID→Int32, Name→String, ProductNumber→String,
[10:17:41] MakeFlag→Boolean, FinishedGoodsFlag→Boolean, Color→String (NULL omitted),
[10:17:41] SafetyStockLevel→Int32, ReorderPoint→Int32, StandardCost→Double,
[10:17:41] ListPrice→Double, Size→String (NULL omitted), Weight→Double (NULL omitted),
[10:17:41] DaysToManufacture→Int32, SellStartDate→Date, rowguid→String(UUID)
[10:17:42] OK Production_Product — 504 documents inserted
[10:17:42] ─────────────────────────────────────────────────────
[10:17:42] [5/5] Starting: Sales.SalesOrderDetail → Sales_SalesOrderDetail
[10:17:42] Type conversions: SalesOrderID→Int32, SalesOrderDetailID→Int32,
[10:17:42] CarrierTrackingNumber→String, OrderQty→Int32, ProductID→Int32,
[10:17:42] SpecialOfferID→Int32, UnitPrice→Double, UnitPriceDiscount→Double,
[10:17:42] LineTotal→Double, rowguid→String(UUID), ModifiedDate→Date(ISODate)
[10:17:44] Batch 1/9: 1000 docs ... Batch 9/9: 327 docs
[10:17:45] OK Sales_SalesOrderDetail — 8,327 documents inserted
[10:17:45] ═════════════════════════════════════════════════════
[10:17:45] MIGRATION COMPLETE
[10:17:45] Total documents inserted : 60,200
[10:17:45] Tables migrated : 5 / 5
[10:17:45] Columns skipped : 2 (hierarchyid x1, geography x1)
[10:17:45] Errors : 0
[10:17:45] Elapsed : 00:00:45
Batch size tuning. The default batch size of 1,000 rows is suitable for most workloads. For very wide rows (>50 columns, large strings), reduce to 500. For narrow tables with high row counts, increasing to 5,000 can improve throughput. Adjust under Settings > Performance > Batch Size.
5

Verify Migration

After migration completes, validate document counts and spot-check data integrity using mongosh or MongoDB Compass.

Document Count Verification

// Connect to target database
use AdventureWorks2019

// Count documents per collection
db.HumanResources_Employee.countDocuments({})
// Expected: 290

db.Person_Address.countDocuments({})
// Expected: 19614

db.Sales_SalesOrderHeader.countDocuments({})
// Expected: 31465

db.Production_Product.countDocuments({})
// Expected: 504

db.Sales_SalesOrderDetail.countDocuments({})
// Expected: 8327

// Quick total across all collections
db.getCollectionNames().forEach(function(c) {
  print(c + ": " + db[c].countDocuments({}))
})

Spot-Check Data Integrity

// Inspect a sample document from HumanResources_Employee
db.HumanResources_Employee.findOne()
// Confirm: OrganizationNode field is ABSENT (was hierarchyid, skipped)
// Confirm: BirthDate is ISODate, not a string

// Verify SalesOrderHeader totals match SQL Server
db.Sales_SalesOrderHeader.aggregate([
  { $group: { _id: null, totalRevenue: { $sum: "$TotalDue" } } }
])
// Compare against SQL Server:
// SELECT SUM(TotalDue) FROM Sales.SalesOrderHeader;

// Verify NULL fields are omitted
db.Production_Product.findOne({ Color: { $exists: false } })
// Should return a product where Color was NULL in SQL Server

// Verify UUID fields are preserved as strings
db.Person_Address.findOne({}, { rowguid: 1 })
// rowguid should be a string like "C55EDB73-23A5-4E00-B0A2-..."

// Check for any skipped-column indicator in a document
db.Person_Address.findOne({}, { SpatialLocation: 1 })
// SpatialLocation should NOT appear in the document

Confirm Collections Exist

db.getCollectionNames()
// Expected output:
// [
//   "HumanResources_Employee",
//   "Person_Address",
//   "Production_Product",
//   "Sales_SalesOrderDetail",
//   "Sales_SalesOrderHeader"
// ]
MongoDB Compass. For visual spot-checking, open MongoDB Compass and connect with the same URI. The Schema tab provides field-level type distribution — use it to confirm DATETIME fields show as Date, BIT fields show as Boolean, and no unexpected nulls appear.
Row count vs. document count. If row counts differ between SQL Server and MongoDB, check the migration log for any BulkWriteError entries. A document exceeding the 16 MB BSON limit will be rejected silently unless ordered:false is set. MigrationBridge logs rejected documents with their primary key value.
6

Post-Migration Tasks

Create Indexes

Indexes are not auto-migrated. After verifying counts, create indexes based on your application's query patterns. Below are recommended indexes for AdventureWorks2019 collections:

use AdventureWorks2019

// HumanResources_Employee — query by LoginID or BusinessEntityID
db.HumanResources_Employee.createIndex({ LoginID: 1 }, { unique: true })
db.HumanResources_Employee.createIndex({ JobTitle: 1 })

// Person_Address — query by City, StateProvinceID, or PostalCode
db.Person_Address.createIndex({ PostalCode: 1 })
db.Person_Address.createIndex({ City: 1, StateProvinceID: 1 })

// Sales_SalesOrderHeader — query by CustomerID, OrderDate, Status
db.Sales_SalesOrderHeader.createIndex({ CustomerID: 1 })
db.Sales_SalesOrderHeader.createIndex({ OrderDate: -1 })
db.Sales_SalesOrderHeader.createIndex({ Status: 1, OrderDate: -1 })

// Sales_SalesOrderDetail — query by ProductID or SalesOrderID
db.Sales_SalesOrderDetail.createIndex({ SalesOrderID: 1 })
db.Sales_SalesOrderDetail.createIndex({ ProductID: 1 })

// Production_Product — query by ProductNumber or Name
db.Production_Product.createIndex({ ProductNumber: 1 }, { unique: true })
db.Production_Product.createIndex({ Name: 1 })

// Verify indexes
db.Sales_SalesOrderHeader.getIndexes()

Atlas Search (Atlas Deployments Only)

If the target is MongoDB Atlas, create Atlas Search indexes for full-text search capability. This replaces SQL Server full-text indexes:

// In Atlas UI: Search → Create Search Index → JSON Editor
// Example: full-text search on Production_Product.Name and Description
{
  "mappings": {
    "dynamic": false,
    "fields": {
      "Name": { "type": "string", "analyzer": "lucene.standard" },
      "ProductNumber": { "type": "string" }
    }
  }
}

Shard Key Recommendations (Sharded Clusters)

For large collections on a sharded MongoDB cluster, choose shard keys carefully. Poor shard key selection causes hotspot shards:

CollectionRecommended Shard KeyRationale
Sales_SalesOrderHeader{ CustomerID: 1, OrderDate: 1 }High cardinality; distributes by customer over time
Sales_SalesOrderDetail{ SalesOrderID: 1 }Co-locates detail rows with header on same shard
Person_Address{ StateProvinceID: 1, PostalCode: 1 }Geographic distribution
Production_ProductNot sharded recommended<1,000 docs; sharding overhead exceeds benefit
Shard key is immutable after sharding. You cannot change a shard key without dropping and recreating the collection. Plan shard keys before enabling sharding. Consult the MongoDB sharding documentation before running sh.shardCollection().

Enable Document Validation (Optional)

MongoDB is schema-less by default. If you want to enforce field types post-migration, add a JSON Schema validator:

db.runCommand({
  collMod: "Sales_SalesOrderHeader",
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["SalesOrderID", "CustomerID", "OrderDate", "TotalDue"],
      properties: {
        SalesOrderID: { bsonType: "int"    },
        CustomerID:   { bsonType: "int"    },
        OrderDate:    { bsonType: "date"   },
        TotalDue:     { bsonType: "double" }
      }
    }
  },
  validationLevel: "moderate",
  validationAction: "warn"
})

🔁 Live CDC Streaming — SQL Server → MongoDB

MigrationBridge Live CDC mode captures ongoing changes from SQL Server's Change Data Capture mechanism and streams them to MongoDB as upsert and delete operations. Use this for zero-downtime cutovers or continuous synchronization.

SQL Server Enterprise or Developer edition required for CDC. CDC is not available on SQL Server Standard or Express. On AWS RDS for SQL Server, CDC requires Multi-AZ or the stored procedure msdb.dbo.rds_cdc_enable_db.

Step 1 — Enable CDC on SQL Server Source

-- Enable CDC on the database
USE [AdventureWorks2019];
EXEC sys.sp_cdc_enable_db;

-- Verify CDC is enabled
SELECT name, is_cdc_enabled FROM sys.databases
WHERE name = N'AdventureWorks2019';

-- Enable CDC on each source table (example: Sales.SalesOrderHeader)
EXEC sys.sp_cdc_enable_table
    @source_schema       = N'Sales',
    @source_name         = N'SalesOrderHeader',
    @role_name           = NULL,
    @supports_net_changes = 1;

-- Repeat for each table that needs live sync
EXEC sys.sp_cdc_enable_table
    @source_schema        = N'Sales',
    @source_name          = N'SalesOrderDetail',
    @role_name            = NULL,
    @supports_net_changes = 1;

-- Verify capture instances
SELECT capture_instance, source_schema, source_table, start_lsn
FROM  cdc.change_tables;

Step 2 — Full-Load First, Then Enable CDC

The recommended sequence for a zero-downtime migration:

  1. Enable CDC on SQL Server source (Step 1 above)
  2. Record the current LSN (log sequence number) before starting full load
  3. Run full table migration with MigrationBridge (Steps 1–5 of this SOP)
  4. After full load, start CDC streaming from the recorded LSN
  5. Let CDC catch up until lag approaches zero
  6. Pause writes on SQL Server, let CDC drain, verify counts, cut over application
-- Record current LSN before starting full load
SELECT sys.fn_cdc_get_max_lsn() AS CurrentMaxLSN;
-- Store this value; pass it to MigrationBridge CDC Start LSN field

Step 3 — Configure CDC Streaming in MigrationBridge

  MigrationBridge — Live CDC Configuration

CDC Settings

Live CDC
0x0000E2A400000A1E0001
2000 ms
500 changes
MongoDB insertOne
MongoDB updateOne (upsert)
MongoDB deleteOne
Start CDC Streaming Stop
[11:02:00] CDC streaming started — polling every 2000ms
[11:02:00] Start LSN: 0x0000E2A400000A1E0001
[11:02:02] Batch polled: 14 changes (12 INSERT, 2 UPDATE)
[11:02:02] → Sales_SalesOrderHeader: 8 upsert, 4 insert
[11:02:02] → Sales_SalesOrderDetail: 2 upsert, 2 insert
[11:02:04] Batch polled: 6 changes (3 INSERT, 3 DELETE)
[11:02:04] → Sales_SalesOrderDetail: 3 delete operations
[11:02:06] Lag: ~0 changes behind — fully caught up

How Change Events Map to MongoDB Operations

CDC Operation CodeSQL Server ChangeMongoDB Operation
1 (Delete)Row deleteddeleteOne({ <pk_field>: value })
2 (Insert)Row insertedinsertOne({ ... })
3 (Before Update)Pre-update imageConsumed internally; not written
4 (After Update)Post-update imagereplaceOne({ <pk_field>: value }, doc, { upsert: true })

CDC Validation Queries

-- Check CDC capture lag (SQL Server side)
SELECT
    ct.capture_instance,
    ct.source_table,
    sys.fn_cdc_get_max_lsn()  AS CurrentLSN,
    ct.start_lsn              AS CaptureStartLSN
FROM  cdc.change_tables ct;

-- Count pending changes not yet sent to MigrationBridge
SELECT COUNT(*) AS PendingChanges
FROM  cdc.dbo_SalesOrderHeader_CT  -- adjust per capture instance name
WHERE __$start_lsn > 0x0000E2A400000A1E0001;  -- replace with last processed LSN
CDC transaction log growth. CDC retains log records until they are consumed. If MigrationBridge stops unexpectedly and CDC polling halts, the SQL Server transaction log will grow without bound until the SQL Server Agent CDC cleanup job runs. Monitor log file usage during extended CDC sessions.

🔄 Type Conversion Reference

MigrationBridge applies the following type coercions when converting SQL Server column values to BSON document fields.

SQL Server Type BSON Type Notes
TINYINTInt32Widened to Int32
SMALLINTInt32Widened to Int32
INTInt32Direct mapping
BIGINTInt64Direct mapping
FLOATDoubleIEEE 754 double
REALDoubleWidened to Double
DECIMAL / NUMERICDoublePrecision may be lost for very large values; Decimal128 option available in settings
MONEY / SMALLMONEYDoubleStored as Double; consider Decimal128 for financial data
BITBoolean0 → false, 1 → true
CHAR / VARCHARStringUTF-8 encoded
NCHAR / NVARCHARStringUTF-16 to UTF-8
TEXTStringDeprecated type; auto-upgraded with INFO log
NTEXTStringDeprecated type; auto-upgraded with INFO log
DATEDate (ISODate)Stored as UTC midnight
DATETIMEDate (ISODate)UTC; millisecond precision
DATETIME2Date (ISODate)UTC; millisecond precision (nanoseconds truncated)
SMALLDATETIMEDate (ISODate)Minute precision preserved
DATETIMEOFFSETDate (ISODate)Converted to UTC; offset stored in separate _tz_offset field
TIMEStringStored as "HH:MM:SS.nnnnnnn"; BSON has no native time type
UNIQUEIDENTIFIERStringStored as lowercase UUID string e.g. "6f9619ff-8b86-d011-b42d-00cf4fc964ff"
BINARY / VARBINARYBinDataBSON BinData subtype 0 (generic)
IMAGEBinDataDeprecated type; auto-upgraded with INFO log
XMLStringRaw XML serialized to UTF-8 string
hierarchyidSKIPPEDColumn omitted; warning logged; no native BSON equivalent
geographySKIPPEDColumn omitted; warning logged; use GeoJSON manually post-migration
geometrySKIPPEDColumn omitted; warning logged
sql_variantSKIPPEDColumn omitted; warning logged; type not deterministic at runtime
NULL (any type)Field omittedDefault behavior; configurable to emit BSON null
Decimal128 option. For financial applications requiring precise decimal representation, enable Settings > MongoDB > Use Decimal128 for DECIMAL/NUMERIC/MONEY. This stores values as BSON Decimal128 instead of Double, preserving full precision. Requires MongoDB 3.4+.
DATETIMEOFFSET and timezone handling. MongoDB Date is always UTC. MigrationBridge converts DATETIMEOFFSET values to UTC and stores the original offset as a companion field named <field>_tz_offset (e.g., ModifiedDate_tz_offset: "+05:30") to preserve the original timezone context.
geography → GeoJSON (post-migration). If you need to preserve spatial data, export the geography columns from SQL Server as WKT (Well-Known Text) using SpatialLocation.ToString() and manually convert to MongoDB GeoJSON format. This is a manual post-migration step not automated by MigrationBridge.

Known Issues & Limitations

Multi-Document Transactions

MigrationBridge uses insertMany without multi-document transactions by default. If a batch fails mid-insert, partial data may be written. Use Settings > MongoDB > Enable Transactions (requires MongoDB 4.0+ replica set or sharded cluster). On MongoDB 3.6, transactions are not available — use idempotent operations and re-run failed batches.

_id Auto-Generated

MongoDB auto-generates an ObjectId for _id on every inserted document. The SQL Server primary key is preserved as a regular field (e.g., SalesOrderID) but is not used as _id. If your application expects _id to equal the source PK, enable Settings > MongoDB > Map PK to _id — this sets _id to the source PK value.

Map PK to _id — uniqueness required. If Map PK to _id is enabled and duplicate PK values exist in the source (e.g., composite keys expressed as string), MigrationBridge will throw a DuplicateKeyError. Only enable this option when the source PK is a single column with guaranteed uniqueness.

No JOINs on Target

MongoDB does not enforce foreign keys or support SQL-style JOINs. Related data that was normalized across tables in SQL Server must be handled via:

Document Order Not Guaranteed

MongoDB does not guarantee insertion order for queries without an explicit sort(). Application queries that relied on SQL Server's clustered index order must add explicit sort directives.

16 MB BSON Document Limit

Each MongoDB document has a hard 16 MB BSON size limit. A SQL Server row with large VARBINARY(MAX) or NVARCHAR(MAX) columns could exceed this limit. MigrationBridge logs rows that exceed 12 MB as warnings (pre-flight) and rejects rows exceeding 16 MB at insert time with an err log entry. Split oversized rows manually using GridFS for binary blobs.

Cosmos DB Mongo API Compatibility

FeatureMongoDB NativeCosmos Mongo API
Wire Protocol4.2+4.2 (subset)
Retryable WritesSupportedMust disable (retrywrites=false)
Transactions4.0+ (replica set)Limited (single shard only)
$lookup (cross-collection)SupportedNot supported on sharded
Change StreamsSupportedPreview — may have latency
GridFSSupportedNot supported
Atlas SearchAtlas onlyUse Azure Cognitive Search instead
Aggregation $facetSupportedSupported (4.2+)
Cosmos DB RU provisioning. High-throughput bulk inserts to Cosmos DB Mongo API will consume Request Units rapidly. If you encounter 429 TooManyRequests errors in the migration log, reduce the batch size to 100–200 or increase the collection's provisioned RUs temporarily during migration.

🖥 UI Behavior During Migration

During an active migration or CDC streaming session, MigrationBridge enters a busy state. The following UI behaviors are by design:

Deduplication on re-run. If a migration run was interrupted and you need to restart, enable Settings > Migration > Skip Existing Documents. MigrationBridge will check for an existing document with the same PK before inserting, skipping rows already present on the target. This makes re-runs safe but adds per-row lookup overhead.

MigrationBridge v1.3.0 · SQL Server → MongoDB · User Guide · Troubleshooting