All versions since 3.2.0
3.2.0
More SQL Server Enhancements. A second gap found while building the SQL Server provider: its triggers carry their action as an inline statement body, not by calling a separate function as PostgreSQL’s do.
Added
Triggernow has an optionalBodyto take an statement body, alongside the existingFunctionthat Postgres uses. The two are mutually exclusive: a trigger either executes a function, or runs an inline body (SQL Server).Bodyis optional and defaults tonull, so the change is source-compatible, and it participates in structural equality (a body change is a drop + recreate, like any other structural trigger change).- The SQL DDL parser and writer accept and emit the inline form:
CREATE TRIGGER … ON s.t AS $$ … $$(dollar-quoted, so the body may contain its own;), in addition to the existing… EXECUTE FUNCTION f(args)form.
3.2.1
Fixed
- Fixed a bug where trailing comments would get merged when formatting DDL. They should now be preserved.
- Fixed a bug where whitespace between comments and statements would get stripped when formatting DDL. It will now collapse to a single blank line.
3.3.0
Changed
- Dropping a schema now emits specific drop instructions for all known elements beneath. This is required for providers that don’t support cascading deletes.
3.4.0
Added
Doctoroperation.NSchemaApplication.Doctor(DoctorArguments)runs read-only health checks against the configured infrastructure.IStateLock.Peek. Reads the held lock (ornullwhen free) without acquiring it, so a diagnostic never contends with a real operation. Added as a default interface method (returnsnull), so existing implementers are source-compatible.- Force-unlock by id.
ForceUnlockArguments.ExpectedLockIdmakesForceUnlocka compare-and-swap: refused with aStateLockMismatchExceptionunless it matches the held lock. Unset keeps the previous “remove whatever is held” behavior.
4.0.0
v4.0.0 is a major release that reworks providers and backends into a new plugin system. This will enable providers and backends to be installed directly from NuGet, independently of the CLI, and pin the versions in your CI.
Added
- Plugin Contract. A new set of interfaces in
NSchema.Pluginsthat will allow providers and backends to declare themselves. - BREAKING: Manual lock holds.
IStateLockHandleis no longer disposable; release is explicit, so a caller can take a lock that outlives the current process (e.g. one front-end command acquires, another releases) simply by never releasing it. - Lock time-to-live.
StateLockRequest.TimeToLiverecords an optional expiry on the resultingStateLockInfo.ExpiresUtc. Expiry is surfaced for visibility but never auto-enforced. - BREAKING: Caller-managed locking. The state lock is acquired by the caller through
app.Locks(IStateLockCoordinator.Acquire(operation, skipLock, …)) and held across the operations it guards, rather than each operation taking its own. - Schema-read seams on the application.
app.CurrentSchema(ICurrentSchemaProvider) reads the recorded (offline) or live (online) schema, andapp.PlanFile(IPlanFileWriter) reads and writes saved plan files — exposed as properties alongsideapp.Operations/app.Locks. - BREAKING: Inspect and release the lock via the coordinator.
IStateLockCoordinatornow manages the whole lock lifecycle throughapp.Locks:Peek(ct)reads the currentStateLockInfo?without acquiring it, andRelease(ct)force-releases whatever is held and returns the released lock’s details. - BREAKING: Operation surface. Every operation is reached through
app.Operations(theINSchemaOperationsfacade) with a uniformXArguments→Result<XResult>shape, and each result carries its outcome as data. - BREAKING: Result & diagnostic model. Operations no longer throw to signal expected outcomes or print their own output. They return
Result/Result<T>carrying success/failure plusDiagnostics, and narrate transient progress throughIProgress<OperationProgress>; the caller decides what to render. UseProgressReporter<TProgress>(). Registers theIProgress<OperationProgress>sink that receives an operation’s transient progress narration, replacing the default no-op reporter — a named builder method alongsideUseSqlGenerator.- Atomic file-state writes. The built-in file state store now writes to a temporary sibling file and atomically renames it into place, so a concurrent reader (e.g. a
planreading the recorded state while anapplycaptures new state) never observes a half-written snapshot. - Public diff reader.
DiffReaderis a public, stateless utility (with a shared.Defaultinstance) that reads aDatabaseDiffinto a renderer-neutralDiffDocument.
Changed
- BREAKING: operations live on
app.Operations, notNSchemaApplication.NSchemaApplicationis now a thin facade exposingServices,Operations(INSchemaOperations),Locks(IStateLockCoordinator),CurrentSchema(ICurrentSchemaProvider), andPlanFile(IPlanFileWriter); the per-operation methods (Plan,Apply,Refresh,Validate,Drift,Import,Doctor) moved ontoapp.Operations. - BREAKING:
IStateLockHandleis no longerIAsyncDisposable. Release is explicit viaValueTask Release(CancellationToken), and the handle now exposesStateLockInfo Info(replacingstring LockId). Operation call sites release in afinally. - BREAKING:
IStateLock.ForceUnlockrenamed toIStateLock.Release, now returningValueTask(wasTask<StateLockInfo?>) to matchIStateLockHandle.Release. Whether a release is “forced” is decided by the seam the caller reaches for —IStateLock.Releaseremoves whatever lock is held;IStateLockHandle.Releaseremoves only its own. StateLockInfogains an optionalExpiresUtc.
Removed
- BREAKING:
NoOpStateLockand the no-op lock fallback.IStateLockis now registered only when a state backend supplies one; an operation either takes a real lock or runs without one, rather than acquiring a placeholder. Operations resolveIStateLock?(optional). - BREAKING: The
ForceUnlockoperation.NSchemaApplication.ForceUnlock,IForceUnlockOperation, andForceUnlockArgumentsare gone; force-unlock is a thin caller ofIStateLock.Release()(the CLI does the peek + expected-id check + confirmation itself). - BREAKING: The
Showoperation.NSchemaApplication.Show,IShowOperation, andShowArgumentsare gone. Reading-and-rendering the recorded state, a saved plan, or (new) the live schema is a thin front-end concern, built on the public read seams above rather than a Core operation. - BREAKING: Renderer interfaces and
Use*Rendererbuilder methods.IDiffRenderer,ISchemaRenderer, andISqlPlanRendererare gone, along withUseDiffRenderer<T>(),UseTerraformRenderer(…),UseSchemaRenderer<T>(), andUseSqlPlanRenderer<T>(). The renderers were never consumed by Core and had no swap points; they are now public concrete utilities (see Added) rather than DI-registered services. A consumer wanting a different format writes its own renderer and calls it directly.
4.0.1
Added
ViewDiff.Materialized. A plain ⇄ materialized view conversion is now carried explicitly on the diff as aValueChange<bool>, andDiffReaderrenders it as a label transition (view → materialized view app.name).
Fixed
- Renamed tables. Constraint, index, and trigger drops (and privilege revokes) on a table being renamed now target the old table name, and execute before the rename.
- Renamed schemas. The schema rename is now ordered ahead of every other action, so drops and revokes inside the renamed schema resolve correctly.
- Renamed materialized views. In-place index drops on a renamed view target the old view name. A rename accompanying a recreate no longer emits a
RenameView(which would have targeted a dropped view); the drop removes the old name and the recreate creates the new one. - View conversions. A plain ⇄ materialized conversion now drops the view as what it currently is, instead of using the desired side’s materialization for the drop.
4.1.0
Added
- Schema template declaration. A
TEMPLATE name BEGIN … END;block allows you to declare a reusable group of objects once. - Schema template application. A
APPLY TEMPLATE name IN SCHEMA a, b;statement instantiates a template into each named schema. - Table templates. A
TEMPLATE name FOR TABLE BEGIN … END;block declares reusable table members. - Index name validation. Duplicate index and index-backed constraint names (primary key, unique, exclusion) within a schema are now rejected at validation time.
DdlDocument.Templatescarries the parsed template constructs for consumers of the reader — aTemplateSetof definitions, applications, and includes.- The formatter lays out
TEMPLATEblocks canonically: header andBEGIN/ENDon their own lines, inner statements formatted as usual and indented one level.
4.2.0
Added
- Data-hazard detection. A new built-in diff policy flags planned 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 attaches raw SQL to a structural change (ADD COLUMN,ALTER COLUMN TYPE, orADD CONSTRAINT) and is spliced into the plan only when the matching change is planned. A block matching nothing is reported as an informational diagnostic and is safe to delete. - Decomposed NOT NULL adds. A required column add with no default and a matching
FOR ADD COLUMNmigration is planned as add-nullable → run the migration SQL →SET NOT NULL, so the add succeeds against a populated table. - Hazard suppression. A matching migration block silences the corresponding data-hazard diagnostic (per trigger; unique-index hazards have no trigger and are never suppressed).
ExecuteDataMigrationplan action carrying the spliced SQL. Executing a plan containing one requires a provider that recognizes it (4.3+); plans with no matched blocks are unaffected.
4.3.1
Fixed
- The diff now shows an added or removed column’s default expression and identity marker, so a column definition reads the same everywhere it appears.
- DDL syntax errors now name the file the error was found in, alongside the existing line and column.
- Import no longer repeats the
CREATE SCHEMAstatement in every object file; only the per-schema header declares the schema. - Import now writes the per-schema header to
<schema>/schema.sqlinstead of<schema>.sql.
4.4.0
Added
- Unified
SCRIPTstatement.SCRIPT '<name>' RUN [ALWAYS | ONCE] ON <event> AS $$…$$;is the new canonical form of deployment scripts and data migrations. The event is a deployment bookend (PRE DEPLOYMENT/POST DEPLOYMENT) or a structural change (ADD COLUMN/ALTER COLUMN TYPE/ADD CONSTRAINTwith a target path); RunConditionon scripts and data migrations, carrying the parsedRUNclause.- The backend state store now carries the recorded script executions.
- A
RUN ONCEscript is recorded on a successful apply and skipped by later plans. A recorded script whose body has since changed stays skipped and warns. - Migrations in schema templates. A
MIGRATIONblock can now be declared inside aTEMPLATE … BEGIN … END;body with an unqualifiedtable.memberpath; applying the template instantiates the block once per target schema. The{schema}token in the block’s SQL is replaced with each target schema’s name.
Changed
- Script names must now be unique across the project (they identify scripts in diagnostics and run-once tracking). A named block declared in a template applied to multiple schemas can include the
{schema}token in its name to keep instances distinct. DdlWriterrenders deployment scripts and named data migrations in theSCRIPTform; anonymous migrations keep the legacy spelling.
Deprecated
- The
PRE|POST DEPLOYMENT '<name>' AS $$…$$;andMIGRATION ['name'] FOR <trigger> <path> AS $$…$$;forms. Both still parse into the same model, and plan/apply/validate now surface adeprecationswarning naming theSCRIPTreplacement. They will be removed in NSchema 5.0.
Fixed
- The formatter no longer re-indents the interior of a dollar-quoted body inside a
TEMPLATEblock, which grew the indentation on every pass and changed the SQL a routine or migration body carries.
4.5.0
Added
- Public state access. The recorded state is now a public model.
ISchemaStateManager, exposed asapp.State, facilitates reading and writing the recorded state, with an optionalReadRaw/WriteRawmethods for moving the serialized payload without interpreting it.
Changed
- Planning with an unreadable state payload now fails with a diagnostic instead of throwing.
4.6.0
Added
- Public desired-schema access.
IDesiredSchemaProvideris now public and exposed asapp.DesiredSchema. HashonScriptandDataMigration. For reading the canonical hash of the body.
Changed
- Refresh no longer silently replaces a state payload it cannot read: it fails unless
RefreshArguments.Forceis set, and a forced replacement carries a warning that the run-once script ledger was reset. The state capture after an apply still replaces an unreadable payload (the SQL has already run), with the same warning.
4.6.1 Latest
Changed
RUN ONCEdeployment scripts that have already been run no-longer show up as an informational diagnostic.