SQL Deploy is a command line (sqldeploy) and a desktop application (SqlDeployGui) over one
engine. This is what the product does today, not what is planned. Where a behaviour is a
deliberate limitation it says so, because most of the design is in what the tool refuses to do.
What it compares
| Objects | How |
|---|---|
| Stored procedures, functions, views, triggers | As text, after the normalisation below, together with the ANSI_NULLS and QUOTED_IDENTIFIER each was created under |
| Tables, indexes, constraints, user-defined types | Through a schema model, not as text — the same column added twice is not the same as adding it once — and emitted as ALTER statements |
| Reference data | Row by row against the table, by key; only the rows that differ are deployed |
| Anything else | Not compared. Permissions, extended properties and ownership are never examined or touched |
Objects present in the database but absent from source are listed in the report and left alone. Nothing is ever dropped.
Comparison rules
The text a server hands back is not the text that was deployed to it, so a naive byte comparison reports almost everything as changed. These rules are what make the comparison mean something.
- The
CREATEprefix is normalised.CREATE PROCEDURE,CREATE OR ALTER PROCEDUREand the length-preserved blank the server stores are all one thing. Only the first occurrence is rewritten; aCREATEinside a string or comment is left alone. - Only the
CREATEbatch is compared.SETstatements before it and anything after the terminatingGO— grants, test calls — are not part of the object. - Encoding is detected from the file, byte-order mark or not, so a UTF-16 file from one editor and a UTF-8 file from another compare equal.
- Line endings and byte-order marks are normalised, and trailing whitespace on each line is removed, so a file re-saved on another operating system is not a change.
- Brackets are optional, and off by default.
[dbo].[Customer]anddbo.Customercan be treated as the same name when the manifest asks for it. - Session settings are compared, not just text. The same text created under different
ANSI_NULLSorQUOTED_IDENTIFIERis a different object, and the generated script always sets both explicitly before each object rather than inheriting a client default. - Determinism. Two runs against an unchanged database produce byte-identical output. Sources are read in ordinal filename order, results are ordered by outcome and name, and no timestamp appears outside the header. The script can be diffed between runs.
The manifest
What to deploy is one YAML file, kept with the code. Where it goes is a separate environment file beside it, so one manifest serves development, UAT and production.
| In the manifest | What it says |
|---|---|
steps |
What is deployed, in order: each step owns one kind of object, a set of schemas, a list of source folders or single files, and optionally its own database or server |
root |
The directory every relative path resolves against, so a manifest in deploy/ can read sources the way the repository lays them out |
ignore |
Patterns for the backup copies, one-off fixes and tooling artifacts nobody intends to manage. Ignored objects are listed, not hidden, and a pattern that swallows a source file raises a warning |
defaults |
Comparison settings every step inherits: bracket handling, default session settings, whether session settings are compared at all |
createEnvironment |
The databases the environment consists of, with their creation scripts, and postCreate steps — what turns a blank server into one the steps can deploy to |
servers |
Logical server names that the environment resolves, so a step can target a mirror or a reporting server without naming it |
meta |
A title and description, carried into the report and the application's title bar |
${NAME} |
Anywhere in a string: replaced from a values section or the process environment, and fails loudly if unset — so a file never holds a password |
| In the environment file | What it says |
|---|---|
target |
Server, default database, integrated or sqlLogin authentication; an empty password is a deliberate "", not an unset one |
allow |
What the desktop application may do here: upgrade, create, drop. Left out, only upgrade |
guiMode |
ask (confirm, the default), approved (deploy without confirming; data loss still asks) or disabled (compare and copy the script, never deploy by clicking) |
defaultDatabases |
Which of the declared databases start ticked for creation |
output |
Where the script and report go, their names, single or per-database scripts, the transaction wrapper, the byte-order mark |
- Environments of your own.
local/<manifest>.<name>.yamlbeside the committed ones is a developer's private target, created from the repository's template, never committed, and shadowing a committed environment of the same name. - Every step needs a database, from its own key or the target's; a step with neither fails by name. A deployment never lands in whatever database a login happens to open.
- A step can be limited to some environments, switched off (
disabled), or compared and validated but kept out of the script (validateOnly).
Step types
| Type | What it does |
|---|---|
procedure, function, view, trigger — or types: [...] for a folder holding several |
Compares each source file to the object in the database and emits only the ones that differ. The declared type is validated: a procedure in a folder called Functions fails the step and names the file, and the type bounds the search for objects that have no source file |
tables |
Builds a schema model from the source — strict, from the files alone, or merge, filling the gaps from the database where the repository owns only part of a schema — and emits the ALTER statements. Merge mode cannot detect removals, and its plan is written out so the exact input can be inspected; it doubles as a migration path to strict |
data |
Rows held as CSV or SQL files, matched by key, in one of four modes: seed (insert what is missing), update, merge (insert and update) or replace. Per-table keys, mode and trigger handling |
scripts |
Files emitted verbatim in filename order, for anything that is safe to run repeatedly |
exec |
One statement emitted verbatim — typically a call that regenerates derived objects, placed after the objects it needs |
- Dependencies are handled, not assumed. A table step that would drop a column a view or procedure depends on takes those dependants into the plan rather than failing at run time.
- Best effort, per step. A source file the schema model cannot hold is left out and reported instead of failing the whole step, so a repository with one awkward file still deploys the rest.
- Several servers, several databases. A step may name its own server or database.
USEis emitted when the database changes; one script is written per server, since a script cannot hop servers without client-specific directives, and each server's run is atomic on its own.
The script
The generated script is the deliverable: it can be reviewed in a merge request, attached to a change record, or handed to whoever owns the database, and it runs the same under a command-line client and in a query window.
- Plain T-SQL with
GOseparators and no client directives — no:setvar, no:on error exit; manifest variables are substituted at generation time. - UTF-8 with a byte-order mark, because without one some command-line clients decode in the
console codepage and silently corrupt every character outside ASCII, string literals included.
bom: falseturns it off. - Progress as it runs. Every step and every object announces itself with
RAISERROR ... WITH NOWAIT, delivered immediately rather than buffered likePRINT. - One transaction, one decision point. The script opens a transaction, refuses to start if one is already open on the connection, and commits only in a clearly marked final batch. Run it without that batch and the result can be inspected on the same connection before anything is permanent.
- A failure stops everything after it. Guards record the reason, say it immediately and set
NOEXEC ON, which persists acrossGO;XACT_ABORTcovers runtime errors; every batch is followed by an@@ERRORcheck so a compile error — which throws nothing — still stops the script and is named. The final batch rolls back and says why, in every client, including one that continues past errors. - Data loss is guarded by default. A change that would drop a column with rows in it stops the
script before it gets there;
--allow-data-lossremoves the guard, and the exit code still says review this. - Per-database scripts where databases have different owners or approvers, so a failure part way through one never leaves another half applied.
- A header that says where it came from: the manifest, the environment, the commit
(
--source-ref), and whether objects were left out of this run by hand.
The command line
| Command | What it does |
|---|---|
sqldeploy script |
Writes the script and the report, and stops. The comparison for a merge request |
sqldeploy deploy |
Writes them, then runs the script — writing the artifacts before it connects, so what ran is on disk even when the run fails |
sqldeploy exec |
Runs .sql files that already exist, for machines with no command-line SQL client |
sqldeploy create |
Builds the environment a manifest declares: the databases, then what they need once they exist |
--validate-only |
Validates every step, deploys and emits nothing — the pull-request guard |
--environment, --server, --database, --out, --source-ref, --mode, --stop-on-error, --best-effort, --allow-data-loss, --no-transaction, --no-bom, --max-parallel |
Later layers win: manifest, then environment file, then command line |
| Exit code | script |
deploy, exec |
|---|---|---|
| 0 | nothing destructive | completed |
| 1 | generation failed; the script is not deployable | errors were raised |
| 2 | deployable, but contains operations that can lose data |
Exit 2 is a signal to review rather than a failure.
The desktop application
SqlDeployGui puts the same engine behind a window. It reads the same manifests and environment
files and writes and runs the same script; nothing it does is unavailable from the command line.
What it adds is seeing the plan before running it, and watching it run.
- The plan as a tree. One row per step with a checkbox, and under it every object the run would change, each with its own checkbox. Unticking an object leaves it out of the script — and the script's header says so. An unticked step is out of the next comparison entirely.
- A diff of each change, shown after the normalisation the comparison applies, so every highlighted line is a difference that counted. A table change is listed element by element with the part of the script that does it; a data change lists the rows per table.
- Steps are compared in parallel and fill in as they finish, so a plan takes about as long as its slowest step. Changing the environment mid-comparison abandons the old one; a server that does not answer never traps the window.
- Environments as a drop-down, listed from the files on disk; New environment… writes a private one from the template and ignores it in git for you.
- Deploy asks first, listing the target, every step with work to do and anything that can lose data, and lets you choose best effort — one object at a time, retried in passes, no transaction — or step through — one step per transaction, pausing between them — for that deployment only.
- Create and deploy in one press on a blank server: the databases are created, the sources compared against what was just built, and the plan deployed; if that comparison fails, nothing is deployed.
- Server output as it arrives, the result of the last deployment, and a fresh comparison afterwards — what still differs is what is still to do.
- Values the configuration needs, such as a password left as
${NAME}, are asked for and kept for the session only; a name that looks like a secret is masked. - The manifest and environment files are editable in place, with comments and layout kept.
- What the application may do is the environment's decision —
allowandguiMode— so a production environment file cannot be used to drop a database by accident, and can be set so production is compared here and deployed only by the release process.
Packaging and automation
- One download per operating system:
SqlDeploy-win-x64-<version>.zipandSqlDeploy-linux-x64-<version>.tar.gz, each holding both the command line and the desktop application, withSHA256SUMSand arelease.jsonmanifest. Framework-dependent: the .NET 10 runtime must be installed. - The About box names the version, build date and commit, and where the settings live, with a Copy button for support.
- The repository builds, checks and releases itself: every push builds and publishes both hosts for both operating systems, and a release is a versioned run uploaded to the package registry.
- A source check guards every build against leaked environment names, local paths, private addresses and passwords, so the repository can be shared.
What is not there
Drift detection — recording what was deployed so later out-of-band changes can be noticed — is not present. Removal of objects is not performed. SQL Server is the only engine.