sqmDataTransfer is a dbatools-based PowerShell module that copies chosen tables from one SQL Server instance to another: it can script the table's DDL on the target if it doesn't exist yet, disables foreign keys and non-clustered indexes for the bulk copy, re-enables them afterward — guaranteed, even on failure — and reconciles row counts between source and target. Every run writes a full log and a self-contained HTML report.
Moving table data between instances by hand means separately handling the target DDL, disabling constraints so the bulk load doesn't choke on FK checks, remembering to re-enable everything afterward even if step 3 fails, and then manually counting rows to prove nothing was lost. sqmDataTransfer runs that whole sequence as one call.
| Without it | With sqmDataTransfer |
|---|---|
| Target table DDL scripted and re-typed by hand, dependencies missed | Table, dependent types, sequences, and FK-referenced tables scripted automatically |
| Bulk copy fails or crawls because FKs/indexes are still active | Non-clustered indexes and foreign keys disabled for the copy, every time |
| A failed step leaves constraints disabled with no one noticing | Re-enable runs in a finally block — guaranteed, even after a failure |
| "I think it copied everything" — no proof | Explicit source-vs-target row-count comparison in the report |
| Partitioned or CLR-typed tables fail the script with an opaque error | Partitioning stripped with a warning; CLR types flagged for manual assembly deploy |
| A 220-table overnight run gets killed halfway — no idea what's already done | -SkipCompleted re-derives that from actual row counts and resumes only what's left |
| A 300M-row table with no primary key can't resume a killed copy without recopying everything | Invoke-sqmChunkedTableTransfer resumes per chunk by row count — no key required |
Verifying the copy means a full COUNT(*) scan on both sides | Row counts read from SQL Server's own partition metadata — no scan, safe at any table size |
Invoke-sqmTableTransfer is the main entry point — it orchestrates the full sequence below for every table you pass in.
Copy-DbaDbTableData.finally block, so this step runs even if an earlier one fails.Every step is logged to %LogPath%\sqmDataTransfer_yyyyMMdd_<FunctionName>.log and returned as a structured result object (Table, Step, Status, Message, Timestamp). A self-contained HTML report is always written at the end of the run — every processed table, anything missing or failed, and the row-count comparison — and opens automatically in the browser unless -NoOpen is passed.
A run over hundreds of tables can get killed halfway — a crashed session, a rebooted box overnight — and the HTML report is only written once the run finishes, so there's normally no record of what actually made it. -SkipCompleted runs Compare-sqmTableRowCount against every requested table up front and skips any where source and target row counts already match, so re-submitting the full table list only touches what's still outstanding. No separate bookkeeping of "which tables finished" — completeness is re-derived from the data itself, every time. Available on Invoke-sqmTableTransfer and as a checkbox in the GUI's options panel.
With -ScriptMetadata, the module doesn't just script the table — it walks the real dependency graph so the CREATE actually succeeds on the target.
| Schema | The destination schema is created automatically if it doesn't exist |
| Dependencies | User-defined types, sequences, and FK-referenced tables the table depends on are scripted automatically too (SMO WithDependencies) |
| Partitioned tables | Still fully scripted and transferred — but the physical partitioning itself (partition function/scheme, per-partition filegroups) is stripped, since there's no way to know whether an equivalent scheme exists on the target. Lands as a normal table on PRIMARY instead of failing with a missing-partition-scheme error. Reported as a warning. |
| CLR user-defined types | Scripted, but flagged as a warning — the assembly itself has to be deployed on the target manually |
| Version targeting | The destination's actual SQL Server version is auto-detected and passed to SMO as TargetServerVersion, so scripting from a newer source (e.g. 2022) down to an older target (e.g. 2019) produces syntax the target can run |
Invoke-sqmTableTransfer is all-or-nothing: if a copy of a 300-million-row table is interrupted an hour in, there's normally no reliable way to know which rows already made it across without a key to anti-join on — and no way to resume without re-copying everything. Invoke-sqmChunkedTableTransfer splits the table by a discriminating column (typically a reporting or snapshot date) and transfers it one distinct value at a time instead.
| Resumability | Before each chunk, source and destination row counts for just that value are compared (COUNT_BIG(*) WHERE [column] = value). A chunk that already matches is skipped — a re-run after a partial failure only touches what's left, with no primary/unique key required anywhere on the table. |
| Interrupted mid-chunk | If a chunk is killed mid-copy (not between chunks), whatever leftover rows already landed for that one chunk are deleted before it's retried — so a retry can never double up rows, even without a key to prevent duplicates. |
| Constraint handling | Foreign keys, indexes, and triggers are disabled once for the whole chunked run and rebuilt once at the end — not per chunk. Rebuilding an index is a whole-table operation regardless of the chunk size, so doing it per chunk instead of once is the difference between minutes and hours on a large table. |
| Column-safe by construction | Each chunk's SELECT is bulk-copied with an explicit name-based column mapping (Invoke-sqmDirectBulkCopy), immune to column order or computed columns on either side — and independent of which dbatools version happens to be installed. |
| Cheap verification | The final consolidated row-count comparison reads SQL Server's own partition metadata instead of scanning — accurate once the run has actually finished, with no COUNT(*) cost at any table size. |
A plain Invoke-sqmTableTransfer call on a table above a configurable row-count threshold (default 10 million, Set-sqmTransferConfig -LargeTableRowThreshold) warns instead of silently doing the all-or-nothing copy — with a ready-to-paste Invoke-sqmChunkedTableTransfer command, including a suggested -ChunkColumn picked from the table's date-typed columns by naming convention. The GUI runs the same check before a transfer starts, since the GUI itself has no chunked path.
| Function | Purpose |
|---|---|
| Invoke-sqmTableTransfer | Main entry point — orchestrates the full five-step sequence for one or more tables |
| Invoke-sqmChunkedTableTransfer | For huge tables without a primary key — splits by a column and transfers/resumes one chunk at a time |
| Export-sqmTableSchema | Scripts table DDL from a source instance (SMO Scripter) |
| New-sqmTableFromScript | Executes scripted DDL batches against a target instance |
| Copy-sqmTableSchema | Convenience wrapper: export + create in one call |
| Disable-sqmTableConstraints | Disables FKs / non-clustered indexes / triggers on a table |
| Enable-sqmTableConstraints | Re-enables (rebuilds) previously disabled FKs / indexes / triggers — re-detects state, nothing to pass in |
| Copy-sqmTableData | Bulk-copies table data (name-based column mapping, dbatools-version independent) |
| Sync-sqmTableData | Syncs a table's actual insert/update/delete delta via a staging table, instead of a full re-copy |
| Compare-sqmTableRowCount | Compares row counts source vs. target for one or more named tables |
| Compare-sqmDatabaseRowCount | Compares row counts for every table between two databases at once — no table list required |
| Export-sqmTransferReport | Builds the per-run HTML summary/row-count report |
| Export-sqmDatabaseComparisonReport | Builds the consolidated database-wide comparison report |
| Show-sqmTableTransferGui | WinForms GUI for the whole workflow |
| Get-sqmTransferConfig / Set-sqmTransferConfig | Module configuration (log path, batch size, large-table threshold, etc.) |
Clustered indexes are never disabled — doing so blocks table access entirely — only non-clustered indexes are touched. Foreign keys and triggers are disabled/enabled individually by name, leaving CHECK/DEFAULT constraints untouched.
# Installation - auto-detects AllUsers (if Admin) or CurrentUser Install.cmd # Transfer two tables, scripting metadata and truncating the target first Invoke-sqmTableTransfer -Source SQL01 -SourceDatabase Sales -Destination SQL02 -DestinationDatabase Sales -Table 'dbo.Orders', 'dbo.Customers' -ScriptMetadata -Truncate -Confirm:$false # Resume an interrupted run over the same table list - already-matching tables are skipped Invoke-sqmTableTransfer -Source SQL01 -SourceDatabase Sales -Destination SQL02 -DestinationDatabase Sales -Table $allTables -ScriptMetadata -SkipCompleted -Confirm:$false # A 300M-row table with no primary key, split by a reporting-date column Invoke-sqmChunkedTableTransfer -Source SQL01 -SourceDatabase Sales -Destination SQL02 -DestinationDatabase Sales -Table dbo.Ergebnis_agg -ChunkColumn Dat_ReportingDate -Confirm:$false # Or via GUI Show-sqmTableTransferGui
Configuration is persisted separately from sqmSQLTool (%APPDATA%\SQLDataTransfer\config.json) so both modules can be imported side by side without interfering, but LogPath/OutputPath default to the same location sqmSQLTool uses.
Built on dbatools — if it's already on the box, sqmDataTransfer is one Import-Module away.
PowerShell source, MIT-licensed. Clone or run Install.cmd to install into your module path.
The core engine this suite is built on — 130 commands for administration, health checks, and compliance reporting.
Every release from 0.1.0.0 to the current version — what changed, and why.
Get in touch about the module, a feature, or the wider powershelldba.de toolchain.