Skip to content

All versions since 3.1.0

3.1.0

SQL Server Enhancements. In working on the upcoming SQL Server provider, some functionality gaps were identified. This release goes towards enabling the SQL Server provider to work without hacks.

Added

  • When modifying a column’s type or nullability, SQL Server requires restating the full column definition. To facilitate this, the AlterColumnType and AlterColumnNullability actions now include both the desired type and nullability. Both are optional and default to null, so the change is source-compatible. A modified column’s ColumnDiff.Definition is now populated with the desired column, and the plan linearizer threads these final values onto the two actions.

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

  • Trigger now has an optional Body to take an statement body, alongside the existing Function that Postgres uses. The two are mutually exclusive: a trigger either executes a function, or runs an inline body (SQL Server). Body is optional and defaults to null, 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

  • Doctor operation. NSchemaApplication.Doctor(DoctorArguments) runs read-only health checks against the configured infrastructure.
  • IStateLock.Peek. Reads the held lock (or null when free) without acquiring it, so a diagnostic never contends with a real operation. Added as a default interface method (returns null), so existing implementers are source-compatible.
  • Force-unlock by id. ForceUnlockArguments.ExpectedLockId makes ForceUnlock a compare-and-swap: refused with a StateLockMismatchException unless 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.Plugins that will allow providers and backends to declare themselves.
  • BREAKING: Manual lock holds. IStateLockHandle is 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.TimeToLive records an optional expiry on the resulting StateLockInfo.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, and app.PlanFile (IPlanFileWriter) reads and writes saved plan files — exposed as properties alongside app.Operations / app.Locks.
  • BREAKING: Inspect and release the lock via the coordinator. IStateLockCoordinator now manages the whole lock lifecycle through app.Locks: Peek(ct) reads the current StateLockInfo? without acquiring it, and Release(ct) force-releases whatever is held and returns the released lock’s details.
  • BREAKING: Operation surface. Every operation is reached through app.Operations (the INSchemaOperations facade) with a uniform XArgumentsResult<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 plus Diagnostics, and narrate transient progress through IProgress<OperationProgress>; the caller decides what to render.
  • UseProgressReporter<TProgress>(). Registers the IProgress<OperationProgress> sink that receives an operation’s transient progress narration, replacing the default no-op reporter — a named builder method alongside UseSqlGenerator.
  • 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 plan reading the recorded state while an apply captures new state) never observes a half-written snapshot.
  • Public diff reader. DiffReader is a public, stateless utility (with a shared .Default instance) that reads a DatabaseDiff into a renderer-neutral DiffDocument.

Changed

  • BREAKING: operations live on app.Operations, not NSchemaApplication. NSchemaApplication is now a thin facade exposing Services, Operations (INSchemaOperations), Locks (IStateLockCoordinator), CurrentSchema (ICurrentSchemaProvider), and PlanFile (IPlanFileWriter); the per-operation methods (Plan, Apply, Refresh, Validate, Drift, Import, Doctor) moved onto app.Operations.
  • BREAKING: IStateLockHandle is no longer IAsyncDisposable. Release is explicit via ValueTask Release(CancellationToken), and the handle now exposes StateLockInfo Info (replacing string LockId). Operation call sites release in a finally.
  • BREAKING: IStateLock.ForceUnlock renamed to IStateLock.Release, now returning ValueTask (was Task<StateLockInfo?>) to match IStateLockHandle.Release. Whether a release is “forced” is decided by the seam the caller reaches for — IStateLock.Release removes whatever lock is held; IStateLockHandle.Release removes only its own.
  • StateLockInfo gains an optional ExpiresUtc.

Removed

  • BREAKING: NoOpStateLock and the no-op lock fallback. IStateLock is 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 resolve IStateLock? (optional).
  • BREAKING: The ForceUnlock operation. NSchemaApplication.ForceUnlock, IForceUnlockOperation, and ForceUnlockArguments are gone; force-unlock is a thin caller of IStateLock.Release() (the CLI does the peek + expected-id check + confirmation itself).
  • BREAKING: The Show operation. NSchemaApplication.Show, IShowOperation, and ShowArguments are 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*Renderer builder methods. IDiffRenderer, ISchemaRenderer, and ISqlPlanRenderer are gone, along with UseDiffRenderer<T>(), UseTerraformRenderer(…), UseSchemaRenderer<T>(), and UseSqlPlanRenderer<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 a ValueChange<bool>, and DiffReader renders 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.Templates carries the parsed template constructs for consumers of the reader — a TemplateSet of definitions, applications, and includes.
  • The formatter lays out TEMPLATE blocks canonically: header and BEGIN/END on 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, or ADD 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 COLUMN migration 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).
  • ExecuteDataMigration plan 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 SCHEMA statement in every object file; only the per-schema header declares the schema.
  • Import now writes the per-schema header to <schema>/schema.sql instead of <schema>.sql.

4.4.0

Added

  • Unified SCRIPT statement. 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 CONSTRAINT with a target path);
  • RunCondition on scripts and data migrations, carrying the parsed RUN clause.
  • The backend state store now carries the recorded script executions.
  • A RUN ONCE script 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 MIGRATION block can now be declared inside a TEMPLATE … BEGIN … END; body with an unqualified table.member path; 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.
  • DdlWriter renders deployment scripts and named data migrations in the SCRIPT form; anonymous migrations keep the legacy spelling.

Deprecated

  • The PRE|POST DEPLOYMENT '<name>' AS $$…$$; and MIGRATION ['name'] FOR <trigger> <path> AS $$…$$; forms. Both still parse into the same model, and plan/apply/validate now surface a deprecations warning naming the SCRIPT replacement. They will be removed in NSchema 5.0.

Fixed

  • The formatter no longer re-indents the interior of a dollar-quoted body inside a TEMPLATE block, 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 as app.State, facilitates reading and writing the recorded state, with an optional ReadRaw/WriteRaw methods 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. IDesiredSchemaProvider is now public and exposed as app.DesiredSchema.
  • Hash on Script and DataMigration. For reading the canonical hash of the body.

Changed

  • Refresh no longer silently replaces a state payload it cannot read: it fails unless RefreshArguments.Force is 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.