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 Latest
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.