All versions since 3.3.0
3.3.0
Added
- Init options. The
initcommand now accepts--provider(postgres,sqlite,sqlserver) and--backend(file,s3) options to scaffold configuration for a specific provider/backend combination. - S3-compatible state stores. The
BACKEND s3block accepts aforce_path_styleattribute for S3-compatible stores (such as MinIO) that require path-style addressing. The endpoint, region, and credentials continue to come from the ambient AWS configuration (AWS_ENDPOINT_URL_S3,AWS_REGION, and the credential chain). - Updated to
NSchema.Core 3.3.0and the latest provider packages.
Fixed
destroynow tears down SQL Server and SQLite projects. Teardown previously failed for these providers because SQL Server’sDROP SCHEMAdoes not cascade, and SQLite cannot drop its implicitmainschema. The migration engine now drops a schema’s contained objects before the schema itself.- DDL formatting (
fmt). Fixed two formatting bugs: comments following the last attribute in a block were flattened onto a single line, and a blank line between a leading comment and its statement was removed.
3.4.0
Added
doctorcommand. A newnschema doctorcommand runs read-only health checks against your declared infrastructure, including database connectivity, state-store reachability, and the state lock. It exits1when any configured check fails, for gating in CI.force-unlock <lock-id>.force-unlocknow accepts the lock id (shown in the blocked operation’s error) and refuses if it no longer matches the held lock — a safety guard against breaking a lock that changed under you. Bareforce-unlockstill releases whatever lock is held. RequiresNSchema.Core 3.4.0andNSchema.Aws 3.2.0.lock-statuscommand. A newnschema lock-statusreports whether the state store is locked. Supports--jsonfor structured output and--detailed-exitcode(exit2when locked) for CI gating.
4.0.0
Version 4.0.0 changes the provider and backend model to function as plugins resolved through the NuGet package manager.
Added
- Third-party providers and backends. A
PROVIDER/BACKENDblock can name any plugin package with asourceattribute. nschema initnow restores plugins.initnow pre-fetches the provider and backend plugins pinned in your config. Operations restore implicitly on first use;initjust does it up front so the first real command is fast.--no-initflag. Skips the implicit plugin restore and requires the plugins to be cached already.lockcommand group.nschema lock status/lock acquire/lock releaseinspect, manually hold, and release the state lock.lock acquireholds a lock that outlives the command (for out-of-band checks before a migration), with an optional--ttl(e.g.30m) and--reason,lock statussurfaces any information about the currently held lock.lock releaserequires the lock id by default (refusing if it no longer matches the held lock), with--forceto release whatever lock is held without naming it.--no-lockflag onapply,refresh, anddestroy. Runs without taking the state lock.nschema state show <file>renders a state file on disk directly, without a configured backend.nschema db showrenders the live database schema, read directly from the database via the provider — the online counterpart tostate show.plugincommand group.nschema plugin listshows the provider and backend plugins your project pins and whether each is restored; plugin show--formatoption (text|json|markdown), selecting the output format for any command.--jsonis now shorthand for--format json.- Markdown output.
--format markdownrenders the plan, SQL, and schema as Markdown for a PR comment or a CI job summary.
Changed
- Providers and backends are now plugins. They ship as separate NuGet packages instead of being bundled with the tool;
nschemarestores the one pinned in your config on first use (it shells out to the .NET SDK to do so). The local-file state backend remains built in. - Scaffolding moved from
inittonschema scaffold. Creating a starter project is nownschema scaffold(initbecame the restore command above). ItsPROVIDER/BACKENDconfig blocks and the sample schema are rendered by the plugins themselves. PROVIDER/BACKENDblocks now require a pinnedversion(the plugin package version); the built-infilebackend is the exception. A first-party label (postgres,sqlite,sqlserver,s3) still resolves to its package automatically.- A
PROVIDERblock is now required to select a provider.NSCHEMA_POSTGRES_CONNECTION_STRINGand the other connection-string variables no longer name the provider on their own — they still override the connection string set in the block. doctorreports plugin problems as diagnostics. A provider or backend that fails to restore or configure is now reported bydoctoras a health-check finding (every such problem at once) instead of aborting on the first.- Lock commands grouped under
lock.lock-status→nschema lock status;force-unlock→nschema lock release, whose prompt is now skipped with--auto-approve/-y(consistent withapply/destroy) instead of--force. The lock-id safety check is unchanged. showsplit by what it shows. The recorded state is nownschema state show(offline; thestatenoun group will growpull/push/move), and a saved plan isnschema plan show <file>. The top-levelshowcommand is gone.completion install/completion uninstallsubcommands replace the--install-autocomplete/--uninstall-autocompleteflags.nschema completion <shell>still prints the script.- Built on
NSchema.Core 4.0.0and the 4.0 provider/backend packages.
Fixed
- Running
nschema --helpin a busy directory like root would cause a performance slowdown due to the--environmentarg autocomplete recursively scanning all the files in the directory. This has been fixed by removing autocomplete. - Torn reads of the local state file. The built-in file state store now writes to a temporary sibling file and atomically renames it into place, so a command reading the recorded state while another run writes it.
Removed
- The
NSCHEMAconfig block.destructive_actionmoved to the--destructive-actionsflag / theNSCHEMA_DESTRUCTIVE_ACTION_POLICYenvironment variable;dialectandtransaction_mode(never wired in) are gone. AnNSCHEMAblock is now rejected as an unknown configuration block. - The top-level
show,lock-status, andforce-unlockcommands, replaced bystate show/plan showand thelockgroup above. Theshow --onlinelive-schema view is nownschema db show(adbnoun group) rather than a mode flag.
4.0.1
Fixed
- Updated to
NSchema.Core 4.0.1which fixes several issues to do with action ordering when objects are renamed.
4.1.0
Added
- Updated to
NSchema.Core 4.1.0which adds support for schema and table templates.
4.2.0
Added
- Data-hazard detection.
planandapply(viaNSchema.Core 4.2.0) now flag changes that are valid against the schema but can fail on the data already in a table.
4.3.0
Added
- Data migrations. A
MIGRATION ['name'] FOR <trigger> <schema>.<table>.<member> AS $$…$$;block (viaNSchema.Core 4.3.0) attaches raw SQL to anADD COLUMN,ALTER COLUMN TYPE, orADD CONSTRAINTchange and runs only when that change is in the plan. A required column add with a matching block is applied as add-nullable → backfill →SET NOT NULL, a matching block silences the corresponding data-hazard warning, and a block matching nothing is reported as safe to delete. The plan output gains a “Data migrations” section (dataMigrationsin--json). Executing a plan with a matched block requires a provider plugin at 4.3 or later.
Changed
- The
importcommand now writes the per-schema header to<schema>/schema.sqlinstead of<schema>.sql.
Fixed
- The
nschema lock releasecommand suggested bylock statusandlock acquirenow carries the--environmentand--directoryarguments of the current invocation. - The diff now shows an added or removed column’s default expression and identity marker.
- DDL syntax errors now name the file the error was found in, alongside the existing line and column.
- The
importcommand no longer repeats theCREATE SCHEMAstatement in every object file; only the per-schema header declares the schema.
4.4.0
Added
- Unified
SCRIPTstatement (viaNSchema.Core 4.4.0).SCRIPT '<name>' RUN [ALWAYS | ONCE] ON <event> AS $$…$$;is the new canonical form of deployment scripts and data migrations: the event isPRE DEPLOYMENT,POST DEPLOYMENT, or a structural change (ADD COLUMN/ALTER COLUMN TYPE/ADD CONSTRAINTwith a target path). - Run-once scripts. A
RUN ONCEscript is recorded in the state backend on a successful apply and skipped by later plans; a recorded script whose body has since changed stays skipped and warns. Plan output marks run-once scripts in the pre/post-deployment sections ((run once);runConditionin--json). Recording requires a state backend — planning without one warns. - Scripts in schema templates. Both script kinds can be declared inside a
TEMPLATE … BEGIN … END;body and instantiate once per applied schema, with the{schema}token substituted in the name and the SQL.
Changed
- Script names must be unique across the project (they identify scripts in diagnostics and run-once tracking); a template-declared script applied to multiple schemas can include
{schema}in its name.
Deprecated
- The
PRE|POST DEPLOYMENT '<name>' AS $$…$$;andMIGRATION ['name'] FOR <trigger> <path> AS $$…$$;forms still work, but plan/apply/validate now surface adeprecationswarning naming theSCRIPTreplacement. They will be removed in NSchema 5.0.
4.5.0
Added
refresh --forcerefresh fails if it finds an unreadable state payload, instead of silently overwriting it;--forcereplaces it, resetting the script ledger.state pull [file]to pull the raw recorded state payload out of the configured backend. Writes to a file or stdout.state push <file>to push the raw recorded state payload into the configured backend. Push takes the state lock (--no-lockto skip).scriptcommand group to manage the scripts recorded in the state:script listshows the recorded scripts (name, execution time, body hash);--jsonemits them as a single array.script hash [name]computes the body hash of the project’s script declarations, bare on stdout for one script, or a listing of all of them, for hand-editing pulled state.script taint <name>removes a script’s record, so it runs again on the next apply.script untaint <name>records a script as executed without running it, using the body hash from the script’s declaration. Taint and untaint take the state lock (--no-lockto skip).
4.5.1
Changed
- A
RUN ONCEscript that has already been run no-longer produces an informational diagnostic.
5.0.0
v5.0 moves the CLI onto NSchema.Core 5.0, whose rearchitecture reshapes configuration, plugins, and planning. Changes below are relative to 4.5.1.
Added
--scopetakes an object, not just a schema. A value is an address —--scope appfor a whole schema,--scope app.ordersfor one object. Read under the NSQL identifier rules, so a quoted segment can carry dots and spaces (--scope '"my.schema"."Order Details"').--ephemeralonplan,apply, anddestroyruns against an in-memory state store discarded when the command exits, standing in for a configuredSTATEstore — for CI pipelines that bootstrap disposable databases. Run-once script history does not persist across runs in this mode.- A lockfile (
nschema.lock) pins declared plugin versions to concrete versions.initresolves eachPLUGIN— a range to the highest available version, an exact pin to itself — and records it; later commands read the pin, so a plugin without a lockfile entry is an error that points toinit. plugin update [<label>]re-resolves ranges to their highest available version and rewrites the lockfile — every plugin, or a single one by label.plugin outdatedshows each plugin’s pinned version against the newest its range allows (whatupdatewould install) and the newest available for this engine.newasks the plugins what it needs to know. A database or state plugin can declare the questions its configuration needs, composing the answers into the statement it writes.--set <key>=<value>onnewanswers a question up front, so a scripted run never blocks. Repeatable.
Changed
- Environments select configuration, not schema.
--environmentlayers the environment’s files over the base configuration; schema files are no longer overlaid per environment. An overlay merges theDATABASE,STATE, orENGINEstatement attributes. DATABASEandSTATEreplacePROVIDERandBACKEND. Each names the thing it configures. The built-in local-file store isSTATE file ( path = '…' );.PLUGINdeclares plugin dependencies.PLUGIN <label> ( source = '…', version = '…' );names the package and pins its version;DATABASE/STATEreference the label. The built-in label-to-package map is gone — every plugin is declared explicitly, andversion/sourceno longer ride the configuring statement. Aversionmay be an exact pin or a NuGet-style range ([5.0,6.0)).ENGINE ( version = '…' );asserts the engine version. A project can require an engine version range; a mismatch fails with a pointer todotnet tool update.newauthors theENGINEassertion. A scaffolded project pins the engine to the CLI’s current major (e.g.ENGINE ( version = '[5.0,6.0)' );), so it fails fast on an incompatible tool.newrunsinitafterwards, resolving and locking the plugins it declared so the project is ready toplanimmediately. Pass--no-initto skip it for an offline or edit-first workflow.- Planning always diffs recorded state against the project, so
plan(and a freshapply) now require both a database and a state store. Planning against the live database is no longer available; userefreshto capture the live schema first. destroyreads the managed schema from the recorded state. The fallback to the working-directory schema when no store was configured is gone, and a state store is now required.- Plan output folds scripts into the diff. Deployment and change-event scripts are first-class parts of the diff, shown in the plan tree (and carried on the
diffobject in--json); the separate pre/post-deployment and data-migration sections are gone. - Policy-blocked plans still render. A plan blocked by policy shows the complete diff and SQL alongside the blocking diagnostics; error severity is what stops an apply.
--destructive-actionsacceptsIgnorealongsideError,Warn, andAllow.- Abbreviated commands are spelled out.
fmtisformat,dbisdatabase, andscaffoldisnew. The old names are gone rather than aliased, so a script still using one fails loudly instead of drifting. applyre-runs policies. The policy flags now apply toapply --plan-filetoo, re-checking the saved plan before executing it.script hash,script taint, andscript untaintoperate on deployment scripts. A template-scoped script is addressed asschema.name, asscript hashlists it.- Errors you can fix are reported as diagnostics; anything else is considered a a bug. A broken configuration file, an unresolvable plugin, an unreachable database — each renders in the diagnostics table, sourced by the file and line or the plugin label that caused it. Anything else reaching the top level is a defect in NSchema: it names the exception type, links the issue tracker, and prints a stack trace unconditionally, so a report needs no re-run with a flag. Messages carry the inner-exception chain, so a cause like
Failed to connect to 127.0.0.1:5432 -> Connection refusedkeeps the part that matters. --jsondistinguishes the two. An expected failure emits a{"type":"diagnostics"}event; an internal error’s{"type":"error"}event gainsexceptionandstack, so a consumer can tell “your project is wrong” from “NSchema is broken” by a null check.
Fixed
- Editing a
PLUGINversion now works correctly.initpreferred the lockfile’s pin unconditionally, so changing a declared version and re-runninginitsilently kept restoring the old one. - An incompatible plugin doesn’t crash the CLI. This CLI will now give a proper explanation of the problem instead.
plugin updateexits non-zero when a restore fails, rather than reporting the problem and returning0.- Plugin loading now resolves a plugin’s native libraries (e.g. SQLite’s
e_sqlite3) from its restored dependency closure. newnames the right environment variable. It pointed at a per-provider variable (NSCHEMA_POSTGRES_CONNECTION_STRING) that no longer has any effect; it now namesNSCHEMA_DATABASE_CONNECTION_STRING.- Contradictory flags fail fast.
--quietwith--verbose, or--jsonwith a non-json--format, are now rejected while parsing. - A failed command always exits non-zero.
lock release,init,new, and theplugincommands reported failures but still exited0. state showreports an error when no state has been recorded yet instead of failing on a missing source.
5.0.1
Fixed
--destructive-actionsargument is ignored. Policy enforcement overrides like--destructive-actionsand--data-hazardsweren’t being respected due to a bug in Core. This is fixed after updating to 5.0.1.
5.1.0
Changed
- Implicit schemas are now ignored. Schemas owned by the database (
dbo,public,main, etc.) will now be ignored during import and planning.
Fixed
- Plugin dependencies are not shared correctly. All plugin dependencies will now be correctly loaded from their own dependency closure where the CLI does not have its own version of an assembly.
5.2.0
Changed
- Plans verify type references. Inherited from NSchema.Core 5.2.0, a plan now checks that every type the project references will exist once it applies.
Fixed
- Schema-qualified engine types no longer block planning. Applying an imported project that referenced engine types (e.g. Postgres’s
pg_catalog.tsvector) previously failed with an error demanding a declaration nobody could write.
5.3.0
Changed
- A create is a create. Built on NSchema.Core 5.3: a plan that believes it is creating a routine or view now renders a plain
CREATE, so colliding with an object the plan didn’t know about fails loudly instead of being silently overwritten; the in-place forms render only when the plan knows it is replacing.
Fixed
- Rebuilding a schema whose functions reference its tables. Routines are now created after the tables they may reference, so applying an imported schema with SQL-language functions (e.g. Pagila) no longer fails with a missing relation.
5.4.0
Added
- Aggregate support.
CREATE AGGREGATEis part of the language (inherited from NSchema.Core 5.4.0). - Plugin restore honors your project’s NuGet configuration. Plugin resolution and restore now run under the project directory, so a
NuGet.Configbeside the project applies as expected.
Fixed
- Functions that call each other now apply in the right order. Inherited from NSchema.Core 5.4: a routine’s definition is scanned for the routines it calls and the objects it reads, and creates are ordered so a callee precedes its caller.
- Plugin restore works inside a repository using central package management. Only your NuGet configuration reaches the restore now; a
Directory.Packages.props(orDirectory.Build.props) further up the tree no longer breaks it with NU1008.
5.5.0
Fixed
- Engine-native SQL bodies no longer show as permanent drift. Inherited from NSchema.Core 5.5.0, hand-written and database-written provider-native SQL (things like view bodies or trigger definitions) are now stored in the state so they can be diffed like-for-like.
5.6.0
Changed
- Full dependency graph support. Updated to NSchema.Core 5.6.0, which refactors the linearizer to be built entirely from the dependency graph.
Fixed
- Imported SQL Server projects read back. Inherits NSchema.Core 5.5.1: multi-statement routine definitions and view bodies survive the import round trip via dollar-quoted bodies, and trailing line comments no longer swallow the closing tokens.
5.6.1
Fixed
- Multi-line doc comments indent correctly. Every line of a
---doc comment on a column or setting now takes the member indent, rather than only the first. - A state payload with no captured schema is rejected. Reading one now fails as an unreadable payload, instead of throwing an NRE.
- Lockfiles are updated additively. Pinning package version now no-longer clobbers packages that weren’t in the updated list, so initializing plugins for one environment won’t remove pins for a different environment.
5.7.0
Changed
applyanddestroycapture the live schema before planning. Before the plan step ofapplyanddestroy, the state is now refreshed. This helps capture drift and also bootstraps the state before a first apply, which is particularly helpful when using the ephemeral store. Replaying a saved plan with--plan-fileis unaffected, since its statements were fixed when the plan was written.
5.7.1
Fixed
- Environment overlays can declare schema again. Database objects declared in
*.env.<name>.sqlwill now join the project alongside the base files, restoring the v4 behavior.
5.8.0
Changed
--quietnow summarizes the run’s output. A quietplanorapplyreports one line per artifact (Plan: 2 to add, 1 to change, 0 to destroy (5 statements).) instead of the full diff and SQL.--quietdrops Info diagnostics. The run-once “already executed” advisories no longer repeat on every planned run.--quietsuppresses the environment banner and secondary hint lines (such aslock acquire’s release hint), which previously printed regardless.
Fixed
--jsonno longer writes the confirmation prompt into the result stream. An interactiveapply --jsonwrote its summary and question to stdout as raw markup, breakingnschema apply --json | jq; the prompt now renders on stderr with the rest of the narration. The same fix keeps a piped--format markdownjob summary free of prompt text. Without a terminal, the summary is reported as a{"type":"log"}event so a redirected stderr stays uniform NDJSON.--jsonreports aDetailline at its own log level. Secondary hint lines were emitted as"level":"announcement", indistinguishable from top-level narration.formatrejects the presentation flags instead of ignoring them.nschema format --json(and--format,--quiet,--verbose) becauseformat’s output is the formatted code.- The
NO_COLORenvironment variable and--no-colorargs should now be respected correctly.
5.9.0
Added
Clustering and XML indexes. Added support for clustering, XML indexes and view indexes, all inherited from NSchema.Core 5.7.
5.10.0
Added
- Plugins can be loaded from a path. A
PLUGINstatement declaringpathloads the assembly directly, skipping the package restore and the shared cache entirely.
5.11.0
Added
- Diagnostic severities can be set in
.editorconfig.nschema_diagnostic.<code>.severityconfigures one finding andnschema_diagnostic_source.<source>.severityevery finding from a producer, taking Roslyn’s severity words (none,silent,suggestion,warning,error,default). A--destructive-actionsflag still wins over the file. - The diagnostics table names each finding’s code, which is what a severity is configured by.
Fixed
importreports its diagnostics whether or not it succeeded. They were shown only on failure, so a successful import had no way to tell you what it could not carry into the project.
5.11.1
Added
plan --jsonnames the action behind each statement. Every entry insqlcarries theactionit performs (CreateTable,AddForeignKey), so what a plan exercises can be read off it rather than inferred from the SQL text. Inherited from NSchema.Core.
Fixed
- An unreadable
.editorconfigno longer crashesinit,new,plan showorcompletion install/uninstall. plan showreports a corrupt plan file instead of crashing on it. Inherited from NSchema.Core: a well-formed JSON object that is not a plan deserialized into nulls, and showing it died with aNullReferenceExceptionin the reporter rather than naming the file..editorconfigseverities now reach the findings that reading a project produces, not only the engine’s.
5.11.2
Fixed
- Migration plan file unable to be deserialized. An accidental breaking change was introduced in NSchema.Core 5.9.1 that now been fixed.
5.11.3
Fixed
- Check constraints no longer cause permanent drift. Check constraints are engine-native SQL, so it gets reformatted by the database. We now store both sides of a check constraint and only compare like to like.
5.11.4
Fixed
- Engines without comment support no-longer cause errors on comments. Engines that don’t support comments now correctly ignore them.
5.11.5
All inherited from the latest NSchema.Core update.
Fixed
- Every remaining authored expression settles. Column defaults and generated expressions, index and exclusion predicates, and a domain’s checks and default are opaque SQL, rewritten by the engine, so a handwritten one would never match. All are now kept and declared like-for-like as they are for triggers, routines etc. An expression the database no longer reports is still drift, not a spelling to restore.
- Renaming a type no longer retypes the columns declared against it. The rename moved the type but left every reference naming the old one, so each column read as a retype.
- Recreate is now correctly blocked by dependents. Recreating a type that’s in use now causes an error.
- An identity that explicitly declares no options no longer differs from one that does so implicitly. No options at all and a set of unstated ones now compare equal.
- A sequence altered for one reason no longer restates the others. The change carries the folded options, so a plan that changes the cache does not also restate a start it never asked to change.
- Identity and sequence restarts now warn correctly. Restarts are data hazards: restarting the counter, means duplicate values are issued, meaning inserts collide with what is already stored.
5.12.0 Latest
Added
- NSQL extension. Project files can now be written as
.nsqlfiles, which can be syntax highlighted using NSchema.Core’s TextMate grammar.
Changed
- Default extension. When scaffolding or importing project files, the default file extension is now
.nsql