MigrationOps is a database schema versioning and source control tool designed to bring structure and traceability to SQL changes.
It combines concepts from Entity Framework migrations and SQL source control tools (such as Redgate SQL Source Control), enabling teams to manage database updates through versioned scripts, checksum validation, and automated execution ordering.
- SQL Source Control: Organize your SQL scripts (stored procedures, views, functions, and triggers) in dedicated folders.
- Automatic Checksum Calculation: Inserts unique checksums into your SQL scripts to easily manage changes.
- Migration Management: Easily handle database migrations with a structured approach, using timestamped filenames to ensure proper execution order.
- Git Integration: Git hooks to automate checksum calculation and ensure valid script tags exist.
- Git: Ensure Git is installed on your machine. You can download it from Git for Windows.
- Git Bash: Git Bash should be installed, as the Git hooks are written in shell script format (
.sh).
To get started, clone the project repository to your local machine:
git clone <repository-url>To ensure that our custom Git hooks are correctly set up on your local environment, please follow these steps:
-
Navigate to the root of the repository
-
Execute the GitHook setup script in PowerShell
.\setup_hooks.ps1- Verify the Hook Make a change in the repository, stage it, and attempt to commit. The pre-commit hook should automatically run and insert/update the checksum in your SQL files.
The pre-commit hook script uses PowerShell's Get-FileHash cmdlet to calculate SHA-256 checksums. This makes the script fully compatible with Windows environments, including GitHub Desktop and Git Bash.
If you're working in a macOS or Linux environment, you may need to modify the script to use a Unix-compatible command like sha256sum. Here’s a basic example:
#!/bin/sh
# Function to calculate SHA-256 checksum using sha256sum
calculate_checksum() {
sha256sum "$1" | awk '{ print $1 }'
}
# Rest of the script...dbconfig.json is used to configure the database connections and migration settings for MigrationOps. It is committed to source control, so it should never contain real credentials — only a template/example shape.
{
"Databases": {
"Db1": {
"ConnectionString": "Server=myServerAddress;Database=db1;User Id=myUsername;Password=myPassword;"
},
"Db2": {
"ConnectionString": "Server=myServerAddress;Database=db2;User Id=myUsername;Password=myPassword;"
}
},
"MigrationSettings": {
"MigrationDirectory": "Migrations",
"ScriptDirectory": "Scripts",
"DefaultDatabase": "Db1"
}
}Real connection strings should never be committed. Configuration is layered, from lowest to highest precedence:
Configurations/dbconfig.json— the committed template above.Configurations/dbconfig.local.json— an optional, git-ignored file with the same shape, for a developer's own local secrets. Only include the keys you want to override:{ "Databases": { "Db1": { "ConnectionString": "Server=.;Database=db1;Trusted_Connection=True;TrustServerCertificate=True;" } } }- Environment variables — recommended for CI/CD pipelines and shared environments. .NET's double-underscore convention maps to the same nested keys, e.g.:
Databases__Db1__ConnectionString="Server=...;Database=db1;User Id=...;Password=...;"
Each layer overrides the one before it, so a value set as an environment variable always wins over the checked-in template.
Place your SQL scripts into the appropriate folders:
- StoredProcedures/
- Views/
- Functions/
- Triggers/
Ensure that all scripts are written using the CREATE OR ALTER statement to simplify deployment.
Scripts will be applied in the order determined by their filenames.
Migrations will be executed in order by datetime, then script number.
Example:
Migrations/20240805-001-CreateNewTestSchema.sql Migrations/20240805-002-CreateEntityTable.sql Migrations/20240806-001-CreateHappyCustomerTable.sql Migrations/20240806-002-CreateCustomerTestTable.sql
Add your migration scripts to the Migrations folder with a filename format that includes a timestamp and a brief description:
Example:
Migrations/20240805-001-CreateEntityTable.sql
When you deploy the project, the migration scripts will be applied to the databases in the Tags comment. Tags should be included as a comment at the top of each SQL script.
The githook runs validation logic to ensure the Tags comment is properly added to each sql file.
Database object scripts are applied before migrations. An object script that fails because it depends on schema a pending migration creates (for example, a view over a table that doesn't exist yet) is deferred and retried automatically after migrations run — so a single deploy works even against a brand-new database. If a script still fails on the retry, the run halts with the script name and nothing further is applied.
Example:
-- Tags: db1, staging
CREATE OR ALTER PROCEDURE dbo.MyProcedure
AS
BEGIN
-- Procedure logic here
END
Requires the .NET 8 SDK.
Build the solution from the repository root:
dotnet build MigrationOps.sln
Run the console app from the MigrationOps.ConsoleApp directory, since Configurations/dbconfig.json and the Migrations folder are resolved relative to the working directory:
cd MigrationOps.ConsoleApp
dotnet run
Running with no arguments opens an interactive menu (choose dry-run / dry-run + verify / apply, then a target database). When stdin is redirected — e.g. from CI — a bare dotnet run performs a full apply, preserving the original behavior.
dotnet run -- apply [--db <name>]
dotnet run -- dry-run [--db <name>] [--verify]
applyruns the deploy pipeline (object scripts, then migrations, then deferred retries).--dblimits it to one configured database.dry-runpreviews what a deploy would do without changing anything: each file is reported as already applied, would apply, CHANGED (an applied migration whose file was edited — a real run would re-execute it), or a validation error (missing-- Tags:/-- Checksum:, object script withoutCREATE OR ALTER). Dry-run never halts early; it collects every problem, prints a per-database summary, and ends withDRY-RUN SUCCEEDED/DRY-RUN FAILED.--verifyadditionally executes the pending scripts against the target database in one transaction per database — so later scripts can rely on earlier scripts' schema — and always rolls back. It needs a reachable database but never commits anything, including history rows.- The exit code is the success flag:
0only when there are no CHANGED, validation-error, or verify-failed entries, makingdry-runsuitable as a CI gate.
MigrationOps.Dashboard is a Razor Pages web app that provides a read-only view of migration state. It reuses the same MigrationOps.Core logic as the console runner, so it shows the exact same per-database picture: applied migration history, pending files, and checksum drift.
-
Create the dashboard's database. The dashboard stores its login accounts in a dedicated database, separate from the migration-target databases. Create an empty database on your SQL Server instance (e.g.
MigrationOpsDashboard):CREATE DATABASE MigrationOpsDashboard;
On first use the app creates its
__DashboardUserstable automatically, but the database itself must already exist. -
Configure the connection string.
MigrationOps.Dashboard/appsettings.jsonis committed with a placeholderDashboardStore:ConnectionString. Don't put real credentials in it — override it with a git-ignoredappsettings.Development.jsonor an environment variable:DashboardStore__ConnectionString="Server=.;Database=MigrationOpsDashboard;Trusted_Connection=True;TrustServerCertificate=True;" -
Check the shared config paths.
appsettings.jsonpoints at the console app's files via relative paths (DbConfigPath,MigrationsRoot), which resolve correctly when you run from theMigrationOps.Dashboarddirectory. The dashboard readsdbconfig.jsonthrough the same layering as the console app, so connection strings indbconfig.local.jsonor environment variables are picked up here too.
cd MigrationOps.Dashboard
dotnet run
The app listens on http://localhost:5280.
Every page requires login. On a fresh install:
- Visit
/Registerto create the first account (minimum 8-character password, stored BCrypt-hashed). - Log in at
/Login.
Registration is a one-time bootstrap, not open signup: once any account exists, /Register permanently redirects to /Login. To add another user later, insert a row into __DashboardUsers manually, or clear the table to re-open registration.
I welcome contributions! Please fork the repository and submit a pull request for any enhancements or bug fixes.
This project is licensed under the MIT License - see the LICENSE file for details.
For any questions or suggestions, please open an issue or reach out to Cat Fortman.