PeopleWorks.SyncJob.Cli
3.0.0
dotnet tool install --global PeopleWorks.SyncJob.Cli --version 3.0.0
dotnet new tool-manifest
dotnet tool install --local PeopleWorks.SyncJob.Cli --version 3.0.0
#tool dotnet:?package=PeopleWorks.SyncJob.Cli&version=3.0.0
nuke :add-package PeopleWorks.SyncJob.Cli --version 3.0.0
<div align="center">
β SyncJob
Move SQL Server data between databases β reliably, safely, and fast.
π Pocket guide β every command on one page
<img src="assets/hero.svg" width="900" alt="Diagram of the SyncJob pipeline: a source SQL Server is read with a SELECT, a stored procedure or an incremental filter; rows stream through SqlBulkCopy into a stage table; MinRowThresholdToCommit decides between COMMIT and ABORT before anything is published; the destination ends up with the final table plus an execution history record. At the bottom, the two publish paths compared on 122,590 rows: TRUNCATE plus INSERT blocks readers for about 15,000 ms, the sp_rename swap never blocks and takes about 370 ms.">
<sub>The rows are already written when the transaction opens, so publishing is a metadata operation β and the row count is checked before it happens.</sub>
</div>
SyncJob moves data between SQL Server databases reliably, safely, and fast. It supports full refresh and incremental sync, runs as a CLI command or a Windows Service, and keeps a full audit trail of every execution. Configuration lives in simple JSON files or in a persistent SQLite database.
It was built for a real problem: getting production data out of a customer's network and into a reporting warehouse, every night, without anyone watching β and without a bad run quietly destroying the destination.
Why it exists
Copying a table is easy. Copying it every night, unattended, without ever leaving the destination in a worse state than before is not.
That distinction drives every design decision here:
| Concern | How SyncJob handles it |
|---|---|
| π‘οΈ A broken source must not wipe the destination | MinRowThresholdToCommit refuses to commit when the source returns fewer rows than expected |
| β‘ Readers must not freeze during a load | Data lands in a stage table, then tables are swapped by name β a metadata operation measured in milliseconds |
| π You must be able to prove what happened | Every run is logged: rows read, inserted, duration, host, error detail |
| π Credentials must not sit in plain text | secrets protect encrypts passwords with Windows DPAPI |
| π§ͺ You must be able to rehearse | validate and --dry-run check connectivity, schema, and mappings without writing a row |
Features
- Two execution modes β CLI for on-demand runs and scheduled tasks; Windows Service for agent-based execution driven by a central server
- Encrypted credentials β passwords protected with Windows DPAPI; the key never lives in the file or the binary
- Non-blocking commit β full refresh publishes by swapping tables by name, so readers are not locked out during the load
- Run a whole config in one command β
run --allwalks every section in file order - Full refresh or incremental sync β sync everything every time, or only the rows that changed since the last run (Timestamp, RowVersion, Change Tracking, CDC)
- Stage / Final two-phase load β data lands in a staging table first, then atomically committed to the final table; or direct mode for single-step inserts
- Parallel bulk load β configurable
MaxDegreeOfParallelismfor high-throughput scenarios - Safety thresholds β
MinRowThresholdToCommitprevents accidental commits when the source returns fewer rows than expected - Dry-run mode β validate connectivity, mappings, and query shape without writing a single row
- Persistent SQLite configuration β store connections, column mappings, and options in a local SQLite database with encrypted passwords (DPAPI)
- Full execution history β every run is logged with row counts, duration, error details, and host machine
- Central sync hub β optional SyncJobCentralDB aggregates execution history from multiple machines for centralized monitoring
- JSON-based config β simple flat JSON files for scripted and DevOps-friendly deployments
- Rich CLI β built on Spectre.Console with colors, tables, progress bars, and panels
Requirements
- .NET 9 Runtime (to run) / .NET 9 SDK (to build)
- SQL Server 2016 or later (source and/or destination)
- Windows (for Windows Service mode and DPAPI password encryption)
Install
Download the binary β a single self-contained executable, no .NET runtime to install:
β¬ Latest release β
syncjob-win-x64.zip
.\SyncJob.exe --version
Or install it as a .NET tool:
dotnet tool install -g PeopleWorks.SyncJob.Cli
syncjob --version
Or build from source:
git clone https://github.com/peopleworks/syncjob.git
cd syncjob
dotnet publish src/SyncJob.Cli/SyncJob.Cli.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ./publish
IncludeNativeLibrariesForSelfExtractis not optional. Without it the single file leaves outMicrosoft.Data.SqlClient.SNI.dllande_sqlite3.dll, and the executable throwsDllNotFoundExceptionthe moment it opens a connection.
Quick Start
1. Build
git clone https://github.com/peopleworks/syncjob.git
cd SyncJob
dotnet publish -c Release -o ./publish
2. Create a config file
{
"SalesSync": {
"Source": {
"ConnectionString": "Server=SOURCE;Database=SourceDB;User Id=sa;Password=...;Encrypt=True;TrustServerCertificate=True;",
"Query": "SELECT Id, Name, Amount, UpdatedAt FROM dbo.Sales"
},
"Destination": {
"ConnectionString": "Server=DEST;Database=DestDB;User Id=sa;Password=...;Encrypt=True;TrustServerCertificate=True;",
"StageTable": "dbo.Sales_Stage",
"FinalTable": "dbo.Sales"
},
"ColumnMappings": [
{ "Source": "Id", "Dest": "Id" },
{ "Source": "Name", "Dest": "Name" },
{ "Source": "Amount", "Dest": "Amount" },
{ "Source": "UpdatedAt", "Dest": "UpdatedAt" }
],
"Options": {
"BatchSize": 10000,
"MaxDegreeOfParallelism": 4,
"BulkCopyTimeoutSeconds": 0,
"KeepIdentity": true,
"MinRowThresholdToCommit": 1000
}
}
}
3. Validate and run
# Validate first (no data is written)
SyncJob.exe validate -c appsettings.json -s SalesSync
# Execute
SyncJob.exe run -c appsettings.json -s SalesSync --direct
CLI Reference
SyncJob.exe --version
SyncJob.exe --help
Every command, its flags, and a copy button for each one live in the pocket guide β one page, ES/EN.
Legacy commands (JSON-based)
Ideal for scripted deployments and Windows Task Scheduler. No database setup required.
| Command | Description |
|---|---|
run |
Execute a sync job |
validate |
Validate config, connectivity, and query shape |
config-init |
Generate a JSON config from a list of field names |
examples |
Show usage examples |
Modern commands (SQLite-based)
Store configuration persistently in a local SQLite database with encrypted passwords.
| Command | Description |
|---|---|
connection add\|list\|test\|delete |
Manage SQL Server connections |
config create\|list\|show\|delete |
Manage sync configurations |
mapping add\|list\|remove\|clear |
Manage column mappings per config |
history list\|show\|stats\|clear |
Browse execution history |
db info\|backup\|restore\|cleanup\|vacuum |
Manage local SQLite database |
central setup\|test\|status\|enable\|disable\|reset |
Central sync hub management |
run-db <CONFIG_ID> |
Execute from SQLite config |
run Options
SyncJob.exe run -c <path> -s <section> [options]
| Option | Description |
|---|---|
-c, --config <PATH> |
JSON config file (default: appsettings.json) |
-s, --section <NAME> |
Section name inside the JSON |
--direct |
Deprecated. Staging always happens; the flag reports that it did nothing |
--append |
Do not truncate Final table before loading |
--dry-run |
Validate only, do not write any data |
--full-refresh |
Ignore incremental tracking, sync everything |
--init-tracking |
Initialize the incremental tracking table |
--top <N> |
Read only N rows from source (testing) |
--batch-size <N> |
Override BatchSize from config |
--maxdop <N> |
Override MaxDegreeOfParallelism |
--min-commit <N> |
Override MinRowThresholdToCommit |
--force-commit |
Commit even if rows < threshold |
--skip-commit |
Load Stage but skip commit to Final |
--log-level <LEVEL> |
Trace / Debug / Info / Warn / Error / Fatal |
--log-file <PATH> |
Write logs to this file |
--log-dir <PATH> |
Log directory (auto-named daily file) |
--json-log |
Write logs in JSONL format |
--quiet |
Suppress console output |
Running every section at once
A config file usually holds several syncs. Chaining them by hand in a script is exactly where one gets forgotten and nobody notices.
# Every section, in file order, stopping at the first failure
SyncJob.exe run -c appsettings.json --all
# Complete the run and report which sections failed at the end
SyncJob.exe run -c appsettings.json --all --continue-on-error
A section counts as syncable when it has both Source and Destination, so unrelated blocks (ConnectionStrings, Logging, AIProxySettings) are skipped automatically.
Stopping at the first failure is the default on purpose: if the fact table did not load, continuing to load its dimensions leaves the warehouse internally inconsistent, which is harder to detect than a clean stop.
Typical Windows Task Scheduler command
SyncJob.exe run -c C:\SyncJob\appsettings.json -s SalesSync --direct --min-commit 0 --log-file C:\Logs\sync.sales.log --log-level Info --json-log --quiet
Stage / Final Load Pattern
Source DB
β
βΌ
Stage Table βββ truncate + bulk insert (safe to fail here)
β
βΌ
Final Table βββ atomic swap (TRUNCATE + INSERT or Stored Procedure)
If the bulk load to Stage fails partway through, the Final table is never touched. Production reads always see a consistent snapshot.
--direct used to skip Stage and write straight to Final. It no longer does, and the flag says so when you pass it. Staging is not overhead: with nothing staged there is nothing for the row guard to compare against, so a source that comes back empty is only discovered after the destination has been emptied. The rows end up in the same table either way β only the order changed.
How the swap works
For a full refresh, SyncJob does not truncate the final table and copy rows into it. It swaps the two tables by name:
Final β Final_swap_a1b2c3d4
Stage β Final β the new data, already written, now published
temporal β Stage β the old data, discarded on the next run
The data was written to Stage before the transaction opened, so the swap itself is a metadata operation.
Why it matters: TRUNCATE takes a schema-modification lock held until commit, and every reader blocks against it β even one using NOLOCK. On a 122,000-row table that measured ~15 seconds of blocked dashboards. The rename swap measured ~370 ms.
β οΈ Indexes travel with the physical table, not the name
sp_renamechanges which table answers to which name. Indexes, constraints and table-level permissions follow the physical table.If you add an index to
Finalonly, after the next swap that index lives onStage. Create indexes on both tables. Grant permissions at schema or role level rather than per table.
SyncJob falls back to TRUNCATE + INSERT β with identical results, just slower β when the swap does not apply:
Finalis a view, not a tableFinalandStageare in different schemas (sp_renamecannot move objects across schemas)--appendmode, where existing rows must be preserved
The log records which path ran: dest.swap.rename or dest.swap.truncate.
Incremental Sync
Enable incremental mode in your JSON config:
"Incremental": {
"Enabled": true,
"Mode": "RowVersion",
"TrackingColumn": "RowVer",
"MergeStrategy": "Upsert",
"PrimaryKeyColumns": ["Id"],
"DeleteDetection": "SoftDelete",
"SoftDeleteColumn": "IsDeleted",
"SoftDeleteValue": "1"
}
Tracking modes
| Mode | How it works | Best for |
|---|---|---|
Timestamp |
WHERE UpdatedAt > @LastSync |
Simple tables with a reliable datetime column |
RowVersion |
WHERE RowVer > 0x{last} |
Production β monotonic, no clock skew |
ChangeTracking |
SQL Server native Change Tracking | When you can enable it on the source DB |
ChangeDataCapture |
SQL Server CDC | Full audit trail including old values |
Merge strategies
| Strategy | Behavior |
|---|---|
Insert |
New rows only |
Upsert |
Insert new + update existing (by primary key) |
Full |
Insert + update + delete |
Delete detection
| Mode | Behavior |
|---|---|
SoftDelete |
Flag column equals value (e.g. IsDeleted = 1) |
AutoDetect |
Uses Change Tracking / CDC events |
Comparison |
PK comparison between source and destination |
SyncJob creates and maintains dbo.SyncJobWatermark in the destination database:
JobId | StepId | Value | PreviousValue | UpdatedAt
First run: reads everything from InitialValue and records where it got to. Subsequent runs: read from that mark forward. PreviousValue is kept beside it because what an operator actually does when a load goes wrong is re-run from where it was before, and without it that means guessing.
Upgrading from an earlier version: the mark used to live in dbo.SyncJobTracking, one row per job, because the engine had no steps. That table is left exactly where it is, with its history; the first run after upgrading starts from InitialValue and reads everything once. See INCREMENTAL_SYNC.md to carry the old value over instead.
SQLite-Based Workflow
# Add connections (passwords protected with DPAPI, per Windows user)
SyncJob.exe connection add source --server SQL01 --database SourceDB --username etl --password "secret"
SyncJob.exe connection add dest --server SQL02 --database DestDB --username etl --password "secret"
# Create a config
SyncJob.exe config create sales \
--display-name "Sales Sync" \
--source-conn source --dest-conn dest \
--source-query "SELECT Id, Name, Amount FROM dbo.Sales" \
--dest-stage dbo.Sales_Stage --dest-final dbo.Sales
# Add column mappings
SyncJob.exe mapping add sales --source Id --dest Id --primary-key
SyncJob.exe mapping add sales --source Name --dest Name
SyncJob.exe mapping add sales --source Amount --dest Amount
# Execute
SyncJob.exe run-db sales --direct
# Review history
SyncJob.exe history stats
π Securing Credentials
Connection strings live in a file on a server. secrets encrypts the passwords with Windows DPAPI β the key is managed by the operating system, never stored in the file or the binary.
# See what is exposed
SyncJob.exe secrets status -c appsettings.json
# Encrypt every password in the file
SyncJob.exe secrets protect -c appsettings.json
Only the password is encrypted. Server, database and user stay readable, because during an incident you need to see where a job points without decrypting anything β and a diff of the file has to stay useful.
{
"Source": {
"ConnectionString": "Server=SRV01;Database=Sales;User Id=etl;Password=enc:u:AQAAANCMnd8BFdER...;"
}
}
Encrypted values carry their own scope marker, so decryption never has to guess:
| Marker | Scope | Who can decrypt |
|---|---|---|
enc:u: |
CurrentUser |
Only the account that encrypted it, on that machine |
enc:m: |
LocalMachine |
Any account on that machine |
| (none) | plain text | Anyone β still works, for backward compatibility |
β οΈ Choosing a scope is not cosmetic
If you encrypt from an interactive session with the default
userscope and the Windows Service runs under a different account, the service cannot read the file.For services, either encrypt with
--scope machine, or encrypt while signed in as the service account.
SyncJob.exe secrets protect -c appsettings.json --scope machine
protect writes a .plain.bak copy the first time it runs. That backup holds the passwords in clear text β move it somewhere safe and delete it from the server. Re-running protect never overwrites that backup, so the original is not lost.
What DPAPI protects: copying the file to another machine is useless β the key does not travel with it. What it does not protect: anyone already executing code as the same user, on the same machine, can read the secret. That is the boundary of the mechanism, and it is worth knowing rather than assuming.
Execution History
Every run is stored automatically:
SyncJob.exe history list # Recent executions
SyncJob.exe history stats # Aggregate per config
SyncJob.exe history show <execution-id> # Full detail
SyncJob.exe history clear --older-than 90 # Remove records older than 90 days
Each record: start/end time, duration, rows read/inserted/updated/deleted/failed, error details, host machine, log file path.
Central Sync Hub
Aggregate execution history from multiple machines into one SQL Server database:
Machine A βββ
Machine B βββΌβββΊ SyncJobCentralDB (SQL Server)
Machine C βββ
Setup on each client:
SyncJob.exe central setup # Interactive wizard
SyncJob.exe central test # Verify connection
SyncJob.exe central enable # Auto-push after every run
SyncJob.exe central status # Show current config
After central enable, every successful run-db automatically pushes its execution record to ExecutionHistory_Central. If the push fails, the sync still succeeds β central sync is fire-and-forget.
In Windows Service mode, the central server can also dispatch sync tasks to connected agents via the SyncTasks table. Agents poll every 30 seconds.
Windows Service Mode
The same binary runs in two modes:
SyncJob.exe β Windows Service (agent mode)
SyncJob.exe run ... β CLI mode
SyncJob.exe run-db ... β CLI mode
Install as a Windows Service:
New-Service -Name "PeopleWorks SyncJob" `
-BinaryPathName "C:\SyncJob\SyncJob.exe" `
-StartupType Automatic `
-DisplayName "PeopleWorks SyncJob Service"
Start-Service "PeopleWorks SyncJob"
The service registers with Windows Event Log under source name "PeopleWorks SyncJob" and uses a 30-second polling loop to pick up tasks from the central database.
Generating a Config from Field Names
# fields.txt β one column name per line
SyncJob.exe config-init \
-f fields.txt \
-o appsettings.json \
-s SalesSync \
--stage dbo.Sales_Stage \
--final dbo.Sales \
--batch-size 10000 \
--maxdop 4 \
--min-commit 1000
Database Management
SyncJob.exe db info # Path, size, schema version, record counts
SyncJob.exe db backup --output <path> # Backup copy (timestamped when --output is omitted)
SyncJob.exe db restore --file <path> # Restore from backup
SyncJob.exe db cleanup --older-than 90 # Remove old history
SyncJob.exe db vacuum # Compact SQLite file
Logging
# JSONL to file, quiet console β ideal for scheduled tasks
SyncJob.exe run -c config.json -s Section \
--json-log \
--log-file C:\Logs\sync.log \
--quiet
# Debug level with auto-dated file in a directory
SyncJob.exe run -c config.json -s Section \
--log-level Debug \
--log-dir C:\Logs
JSONL output is compatible with log aggregators like Loki, Splunk, and Azure Monitor.
Full JSON Config Reference
{
"SectionName": {
"Source": {
"ConnectionString": "Server=...;Database=...;User Id=...;Password=...;Encrypt=True;TrustServerCertificate=True;",
"Query": "SELECT col1, col2 FROM dbo.SourceView",
"StoredProcedure": null,
"Parameters": {}
},
"Destination": {
"ConnectionString": "Server=...;Database=...;User Id=...;Password=...;Encrypt=True;TrustServerCertificate=True;",
"StageTable": "dbo.TableName_Stage",
"FinalTable": "dbo.TableName"
},
"ColumnMappings": [
{ "Source": "SourceCol", "Dest": "DestCol" }
],
"Options": {
"BatchSize": 10000,
"MaxDegreeOfParallelism": 4,
"BulkCopyTimeoutSeconds": 0,
"KeepIdentity": true,
"MinRowThresholdToCommit": 1000
},
"Incremental": {
"Enabled": false,
"Mode": "Timestamp",
"TrackingColumn": "UpdatedAt",
"MergeStrategy": "Upsert",
"PrimaryKeyColumns": ["Id"],
"DeleteDetection": "None",
"SoftDeleteColumn": null,
"SoftDeleteValue": null
}
}
}
Multiple sections in one file are supported. Use -s SectionName to select which one to run.
Column Mappings
Mappings are explicit by default, and required when source and destination names differ or when you want to move a subset of columns.
"ColumnMappings": [
{ "Source": "Id", "Dest": "CustomerId" },
{ "Source": "FullName", "Dest": "Name" }
]
When the names match one-to-one, leave the list out and SyncJob derives it from the source:
"ColumnMappings": [] // or omit the property entirely
It reads the result metadata with CommandBehavior.SchemaOnly, so SQL Server returns the column list without executing the query β free even when the source holds millions of rows.
Hand-writing thirty column names is exactly where a typo hides until the data comes out shifted by one.
The PeopleWorks database tools
SyncJob is one of three .NET CLIs that each solve a different stage of the same modernisation. All three are MIT-licensed, and each one ships its whole command surface as a single-page guide.
| DBFSync | SQLDiff | SyncJob (this repo) | |
|---|---|---|---|
| Moves | Legacy data out of DBF files | Structure β DDL | Data β DML |
| Source | Visual FoxPro DBF, via the x86 ODBC driver | SQL Server schema | SQL Server |
| Destination | PostgreSQL, SQL Server or SQLite | A data-preserving ALTER script |
SQL Server |
| Safety model | One transaction per table, changes detected by SHA-256 | Drops gated, transactional apply, drift exits 2 for CI |
Stage/final load, row-count threshold, --dry-run |
| Runs as | CLI, Windows win-x86, .NET 10 |
Single-file CLI, .NET 9 | CLI and a Windows Service, .NET 9 |
| Pocket guide | π peopleworks.github.io/DBFSync | π peopleworks.github.io/SqlSchemaDiff | π peopleworks.github.io/syncjob |
They chain, in that order:
- SQLDiff brings the relational schema to the expected shape and catches drift between environments before anything touches the data.
- DBFSync loads the Visual FoxPro DBFs onto that schema and keeps them in step while the legacy ERP stays in production.
- SyncJob moves those now-relational rows on to the other SQL Server systems that consume them.
π Companion tool β SQLDiff
SyncJob moves data. Its sibling, SQLDiff, moves structure.
| SQLDiff | SyncJob | |
|---|---|---|
| Moves | Schema β DDL | Data β DML |
| Answers | "Do these two databases have the same shape?" | "Does the destination have the same rows?" |
| Output | A T-SQL migration script you read before running | Rows in a table, with an audit trail |
They are not just related by topic β they hand work to each other:
1. Standing up a destination
Before SyncJob can move a row, the destination tables have to exist with the right shape. Instead of hand-writing DDL, extract it from the source and apply it:
SQLDiff.exe extract --conn "Server=SRC;Database=Prod;..." --out src.sql --json src.json
SQLDiff.exe deploy --source src.json --target "Server=DW;Database=Reporting;..."
2. Keeping stage and final identical
SyncJob's full refresh publishes by swapping tables by name, which
requires Stage and Final to have the same columns in the same order. Drift between
them is exactly the failure SQLDiff is built to catch:
SQLDiff.exe drift --source "...Database=DW;" --target "...Database=DW;" --include Sales,Sales_Stage
3. Catching schema drift before the nightly run
When a column is added to a source view, SyncJob's automatic mappings pick it up β and the
bulk copy then fails because the destination table does not have it. drift exits with
code 2 when the databases diverge, so a scheduled job can check first and stop early:
SQLDiff.exe drift --source "..." --target "..." || exit 1
SyncJob.exe run -c appsettings.json --all
Structure first, then data. A sync into a destination whose shape has drifted either fails loudly or, worse, succeeds into the wrong columns. Checking the shape costs seconds.
Production Checklist
- Run
validateon new configs before the firstrun - Set
MinRowThresholdToCommitto a meaningful value (not 0) β this is the guard that stops a broken source from wiping the destination - Run
secrets protectso no password sits in clear text on the server - Move the
.plain.bakoff the server after encrypting - If a Windows Service will read the config, encrypt with
--scope machineor from the service account - Test with
--top 100on large tables before the first full load - Enable
--json-logand direct--log-fileto a monitored path - Back up the destination before the first full load
- For incremental sync, initialize tracking first:
--init-tracking - Confirm the destination account has
BULK INSERTand table-level write permissions - If you added indexes to a final table, create them on the stage table too (see How the swap works)
What's New in 3.0.0
The engine comes out of the CLI and becomes a package. PeopleWorks.SyncJob.Core is
published alongside this release, and the CLI, the Windows service and the central
command all run on it β there were three copies of the pipeline and they had drifted.
Three things change behaviour. CHANGELOG.md has the migration for each.
| Change | |
|---|---|
| π΄ | The Windows service could empty a production table and report success. It published with TRUNCATE + INSERT ... SELECT *, and its only check asked whether the bulk copy had lost rows β not whether the load was plausible. A source returning nothing gave 0 == 0 and passed. 2.3.0's notes said the duplication was gone; there were three copies and only one had been fixed. |
| π΄ | The watermark moved to dbo.SyncJobWatermark, keyed per step. Your old dbo.SyncJobTracking is untouched, history and all; the first run after upgrading reads everything once unless you carry the value across. |
| π΄ | "Mode": "Timestamp" could not be loaded at all β the form this repository's own documentation teaches threw before anything was validated. |
| π | --direct no longer skips staging, and says so. With nothing staged there is nothing for the guard to compare, so an empty source is discovered after the destination is emptied. |
| π | Catalog passwords are re-protected with DPAPI. They were XOR'd against a key that was a literal in the source. A SQL-auth connection created by the CLI had never been runnable; it is now. |
| π’ | Streaming copy β one million rows at 0.0 MB of live heap, against 182.7 MB for the list it replaces, and faster. A table larger than RAM is copyable at last. |
| π’ | Publication by SWITCH β a reader blocked 31 ms against 1,137 ms for truncate-and-insert, and the destination keeps its indexes and GRANTs. |
| π’ | A job lease replacing the "in progress" flag a crashed run left set for ever. |
| π’ | 245 unit and 122 live tests. This repository had none. |
Version
SyncJob.exe --version
Version is embedded at build time via the .csproj β update <Version> in one place and it propagates everywhere.
License
MIT β see LICENSE for details.
Contributing
Pull requests are welcome. For major changes, open an issue first to discuss the approach.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit your changes
- Open a Pull Request
<div align="center">
Built by PeopleWorks
Created by Pedro HernΓ‘ndez β PeopleWorks, Microsoft MVP for .NET
Built with .NET 9 Β· Spectre.Console Β· Microsoft.Data.SqlClient
PeopleWorks database tools β DBFSync moves the legacy data Β· SQLDiff moves the schema Β· SyncJob moves the data
π DBFSync guide Β· π SQLDiff guide Β· π SyncJob guide
Every feature in this tool came from running it in production, not from a whiteboard.
MIT licensed β use it, fork it, ship it.
Β© 2026 PeopleWorks
</div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0 | 67 | 9/7/2026 |