Skip to content

All versions since 3.3.0

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.