Upgrading to v4
NSchema v4 turns providers and backends into plugins backed by independent NuGet packages. Packages are fetched at runtime, instead of compiling them directly into the CLI. That decouples a database engine provider’s release cadence from the tool’s, lets you pin an exact provider version, and lets you bring your own. This release also tidies up the command surface and removes the last of the non-config-in-SQL configuration.
This page lists everything that changed and the concrete edits you need to make. Most projects need three changes:
- Add a
versionto eachPROVIDER/BACKENDblock. - Drop the
NSCHEMAblock, - Update a handful of command names in your scripts and CI.
Providers and backends are now plugins
Section titled “Providers and backends are now plugins”In v3 every provider shipped inside the CLI. In v4 they are separate NuGet packages that nschema restores on first
use (it shells out to the .NET SDK to do so, so your standard NuGet configuration is conserved). Name and pin each one
in your config:
PROVIDER postgres ( version = '4.0.0', -- new: required connection_string = '');
BACKEND s3 ( version = '4.0.0', -- new: required (every backend except `file`) bucket = 'my-state');What you need to do:
- Add a
versionto everyPROVIDERblock and every non-fileBACKENDblock to specify the plugin package version. The built-in local-filebackend stays built in: no version, no plugin. - First-party labels (
postgres,sqlite,sqlserver,s3) still resolve to their packages automatically. To use a third-party plugin, point asourceattribute at its package id. - Make sure the .NET SDK is on your
PATHwherevernschemaruns (including CI) because the restore shells out to it. Your existing NuGet configuration (private feeds, etc.) is respected. - Run
nschema initonce up front to pre-fetch the plugins so the first real command isn’t delayed (operations restore implicitly anyway). Use--no-initto require the cache and skip the restore.
A PROVIDER block is now required
Section titled “A PROVIDER block is now required”In v3, setting NSCHEMA_POSTGRES_CONNECTION_STRING alone was enough to select Postgres. In v4 a connection-string
environment variable no longer self-identifies a provider: you must declare a PROVIDER block (with its version).
The env var still overrides the connection string set in the block, so secrets stay out of your .sql files.
The NSCHEMA config block is gone
Section titled “The NSCHEMA config block is gone”The NSCHEMA ( … ) block has been removed entirely. Configuration in v4 is just PROVIDER + BACKEND
(see Configuration blocks).
destructive_actionmoved out of config. Set it with the--destructive-actionsflag or theNSCHEMA_DESTRUCTIVE_ACTION_POLICYenvironment variable. See Destructive-action safety.dialect/transaction_modewere never wired in and are gone too.
An NSCHEMA block now errors as an unknown configuration block, so delete it.
Command changes
Section titled “Command changes”The operations are unchanged, but several command names moved. Update your scripts, aliases, and CI.
Scaffolding a project: init → scaffold
Section titled “Scaffolding a project: init → scaffold”Creating a starter project is now nschema scaffold. nschema init
became the plugin-restore command described above. The scaffolded PROVIDER / BACKEND blocks and sample schema are
rendered by the plugins themselves.
| v3 | v4 |
|---|---|
nschema init |
nschema scaffold |
| (implicit) | nschema init (restore plugins) |
Lock commands grouped under lock
Section titled “Lock commands grouped under lock”The lock verbs now live under a lock noun group:
| v3 | v4 |
|---|---|
nschema lock-status |
nschema lock status |
nschema force-unlock <id> |
nschema lock release <id> |
lock release skips its confirmation prompt with --auto-approve / -y (consistent
with apply and destroy) instead of the old --force. The lock-id safety check is unchanged. v4 also adds
lock acquire to hold a lock for out-of-band coordination, and a
--no-lock flag on apply/refresh/destroy to run without taking the lock.
show split by what it shows
Section titled “show split by what it shows”The single show command is gone, split by the thing it renders:
| v3 | v4 |
|---|---|
nschema show |
nschema state show |
nschema show <plan-file> |
nschema plan show <file> |
nschema show --online |
nschema db show |
state show reads the recorded state offline (the state noun group will grow pull/push/move); plan show
renders a saved plan file; and the live-schema view that was show --online is now its own command,
db show, under a db noun group, read directly from the database through the provider.
Completion: flags → subcommands
Section titled “Completion: flags → subcommands”Installing shell completion is now a pair of subcommands instead of flags:
| v3 | v4 |
|---|---|
nschema completion --install-autocomplete |
nschema completion install <shell> |
nschema completion --uninstall-autocomplete |
nschema completion uninstall <shell> |
nschema completion <shell> still prints the raw script to stdout.
Other behavior changes
Section titled “Other behavior changes”doctorreports plugin problems as diagnostics. A provider or backend that fails to restore or configure is now surfaced bydoctoras a health-check finding (all such problems at once) instead of aborting on the first one.
Embedding the engine (library API)
Section titled “Embedding the engine (library API)”If you drive NSchema.Core directly instead of through the CLI, v4 reworks the application surface. See
Configuration (C#) for the current shape; the breaking changes are:
- Operations moved to
app.Operationsand returnResult<T>.await app.Apply(args)becomesawait app.Operations.Apply(args), and the like forPlan/Refresh/Validate/Drift/Import/Doctor. An expected failure (a policy violation, lock contention) now comes back asresult.IsFailurewith diagnostics instead of throwing. Applynow requires a plan.ApplyArgumentsnow takes a requiredSql(aSqlPlan). The flow is two calls:var plan = await app.Operations.Plan(new PlanArguments { Target = PlanTarget.Live });thenawait app.Operations.Apply(new ApplyArguments { Sql = plan.Value!.Sql! });.- Saved plans round-trip through
app.PlanFile.ApplyArguments.PlanFileis gone; read a saved plan withawait app.PlanFile.Read(path, ct)and applyenvelope.Sql. Writing is stillPlanArguments.OutFile. PlanDestroy/Destroyare gone — a teardown is aPlanwithTarget = PlanTarget.Teardown, applied like any plan.Show/ForceUnlockare gone too: read state viaapp.CurrentSchema/app.PlanFile, and manage the lock viaapp.Locks.IOperationReporter/UseReporterare gone. Structured output is in theResult<T>you serialize yourself; for live progress, register anIProgress<OperationProgress>withUseProgressReporter<T>().NSchemaApplicationOptions.ExceptionBehavioris gone. Expected failures areResultfailures; genuine exceptions still propagate out of the call. The engine prints nothing itself.- The application is reusable. The single-use guard is removed — run multiple operations through one instance, then dispose it.
Checklist
Section titled “Checklist”- Add a pinned
versionto everyPROVIDERblock and non-fileBACKENDblock. - Ensure the .NET SDK is on
PATHeverywherenschemaruns, thennschema initto pre-fetch plugins. - Add a
PROVIDERblock anywhere you previously relied on a connection-string env var alone. - Delete the
NSCHEMAblock; movedestructive_actionto--destructive-actions/NSCHEMA_DESTRUCTIVE_ACTION_POLICY. - Update command names in scripts and CI:
init→scaffold,lock-status→lock status,force-unlock→lock release,show→state show/plan show, and the completion install/uninstall flags → subcommands.