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 | Source and destination row counts per chunk value are snapshotted once, up front, in a single GROUP BY scan per side, not re-queried per chunk. A chunk whose counts already match 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. That warning only fires once the destination already holds a meaningful share of the source's rows (default 30%, Set-sqmTransferConfig -ChunkAdviceMinExistingPercent), since chunking's advantage is resumability, on a fresh/empty destination there's nothing to resume, and the plain copy is simply faster. 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.