Skip to content

All versions since 4.3.0

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

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.

4.6.1

Changed

  • RUN ONCE deployment scripts that have already been run no-longer show up as an informational diagnostic.

5.0.0

v5.0 is a Core rearchitecture, aiming for better project health, with clear separation between layers and a better separation of concerns.

Changed

  • Any policy’s findings can be configured, not just the built-in ones. WithDiagnostic(code, enforcement) configures one finding and WithDiagnosticsFrom(source, enforcement) a producer’s whole family, replacing DestructiveActionOptions and DataHazardOptions — a policy no longer needs its own options type and a builder method to be configurable, so a policy you register is configurable too. A policy reports at the severity it judges natural and the engine applies the override, so WithDestructiveActions and WithDataHazards remain as named shortcuts for the two built-ins.
  • A diagnostic’s Source is what produced it, and only that. The differ reported findings under four topics (scope, directives, run-once, data-migrations) where the producer was the differ; those topics now live in the code, which is where a finding’s identity belongs.
  • Column alterations are unified. Dialects now receive a single AlterColumn action for a column’s type and nullability changes, which they can render as one or more statements.
  • Member diffs are built by factory. PrimaryKeyDiff, ForeignKeyDiff, UniqueConstraintDiff, CheckConstraintDiff, ExclusionConstraintDiff, IndexDiff and TriggerDiff expose Added(definition), Removed(name) and CommentChanged(name, change) in place of a constructor.
  • Object diffs are built by factory too. Every schema-level diff — table, column, view, enum, domain, sequence, routine, composite type, extension and schema is constructed through Added / Removed / Modified and refined with with, replacing constructors.
  • Better domain mode naming. Database and Schema are now the main entry points into the domain model.
  • The SQL dialect seam is the abstract SqlDialect class. A dialect overrides one method per migration action. Standard SQL is rendered by the base through an overridable identifier-quoting kernel.
  • Identifiers are case-sensitive. An identifier’s identity is its exact written text: users and Users are no-longer considered equivalent.
  • Identifiers can be quoted. CREATE TABLE app."Order Details" ("weird ""col""" int) all work, and lets a name collide with a keyword. Square brackets are accepted as an alternative spelling, so CREATE TABLE app.[Order Details] ([weird ]]col]]] int) reads the same (]] escapes a bracket). Quotes and brackets are syntax, not identity: casing is significant with or without them, and either spelling names the same object. The writer (and import) quotes only names that need it, always with double quotes, and extension names now render as quoted identifiers rather than string literals.
  • Parsing is lossless. The syntax tree now preserves every character of the source, including comments, whitespace, and layout.
  • Management directives. The language now separates declarations (what the schema is) from directives (how the difference is managed). This includes RENAME and SCRIPT.
  • Every namespace has moved. Namespaces are vertically sliced of the form NSchema.<Feature>.<Capability>.
  • A slice’s domain layer is Domain, not Model.
  • The shared primitives live in NSchema.Diagnostics.
  • The schema model is NSchema.Model now. It owns the top-level domain model for databases.
  • The model is layered by what contains what. A DatabaseObject belongs to the database, a SchemaObject to a schema, and an ObjectMember to a schema object.
  • An element’s Address is never null. A schema object addresses itself as an ObjectAddress and a member as a MemberAddress, so neither needs narrowing or a cast.
  • A kind belongs to one level. DatabaseObjectKind, SchemaObjectKind and MemberKind replace the single ObjectKind, which spanned schemas’ contents and the database’s own.
  • An address is named for where it sits, not for what it names. DatabaseAddress replaces SchemaAddress and carries the kind as data (DatabaseAddress.Schema(name), DatabaseAddress.Extension(name)), so a new database-level kind needs no new address type. MemberAddress takes an optional kind, matching ObjectAddress.
  • A script is identified, not addressed. A script decorates the schema rather than living in it, so ScriptReference replaces the address it used to carry and Script.Reference replaces Script.Address. A script’s scope is a schema name rather than an address, and the run-once ledger keys on the reference. With extensions addressed as the database objects they are, no address carries an optional schema any more.
  • The diff interfaces name the same levels as the model. IDatabaseElementDiff replaces INamedObjectDiff as what every diff node shares, and IObjectMemberDiff is the member level — so an index and a trigger diff sit with the columns and constraints rather than alongside an extension. IMigratableDiff narrows to a member diff that can carry a script, and TableDiff.EnumerateMembers() returns the member abstraction.
  • A diff node’s Change replaces its Kind. Kind now means what sort of thing something is, so the diff says what happened to it instead: diff.Change == ChangeKind.Add.
  • ChangeKind.Touched replaces a null change. A schema in the diff only because its contents changed is Touched rather than carrying no change at all, so SchemaDiff.Change is always present and IDatabaseObjectDiff covers both schemas and extensions. Touched is the enum’s default, so an unset change reads as “unchanged” rather than “create this”.
  • DataMigrations are Scripts now. This reflects the syntax changes introduce in [4.4.0] so the model becomes consistent.
  • Templates accept object-level directives. A TEMPLATE body may now contain the object-level RENAME directive (table, column, view, enum, domain, type, sequence, routine) alongside its declarations and scripts.
  • Scripts split into ChangeScript and DeploymentScript. Script is now an abstract base carrying the common behavior (name, SQL, scope, hash, reference, run condition);
  • “Desired” is Project now. IDesiredSchemaProvider becomes IProjectProvider — the project is the desired state by definition, and app.DesiredSchema becomes app.Project, named for what it hands you like every surface beside it. Progress output follows: Loading project..., Validating project..., and the plan census now reads Declared: / Recorded state:.
  • AddDdlSchemas is AddProjectSource now. The files describe the whole project (schema, scripts, templates, config), not just schema DDL.
  • Result use consistency. Lots of interfaces have been neatened up to return a Result<T> instead of throwing to allow for error/warning accumulation.
  • The diff now includes scripts. Rather than being tacked on to the plan, scripts are now a first-class part of the diff, carried where they run rather than in a central list.
  • Cohesive plan artifact. There’s now a single MigrationPlan model that represents the plan in its entirety rather than being spread across SqlPlan, PlannedMigration, etc.
  • Providers are required. Providers are now required for planning, because the SQL is built into the plan model.
  • Plan errors are non-blocking. Even when the plan has errors, you can now still access the resultant plan.
  • Planning always diffs recorded state against the project. There is no longer an option to plan against the live database.
  • A teardown is a plan towards an empty schema. PlanTarget.Empty replaces PlanTarget.Teardown, and it obeys PlanArguments.Scope like any other plan.
  • Teardown plans run the policies. A teardown is fully destructive, so the default destructive-action policy blocks it; the blocked result still carries the complete plan. Set the destructive-action enforcement to Allow to apply one.
  • Policies are enforced at apply. Apply now re-runs all policies against the plan diff before executing.
  • Policies now cover project and plan. IProjectPolicy replaces ISchemaPolicy and IPlanPolicy replaces IDiffPolicy.
  • SqlDialect replaces ISqlGenerator. (registered with UseSqlDialect<T>()).
  • IStateLockManager replaces IStateLockCoordinator. Lines up with with ISchemaStateManager.
  • IPlanFileManager replaces IPlanFileWriter. It reads saved plans too, so “writer” undersold it.
  • UseFileState replaces UseFileStateStore. It registers the lock alongside the store, so “store” undersold it — matching UseEphemeralState, which does the same. UseStateStore and UseStateLock still register a single seam each.
  • TableDiff.PrimaryKeys replaces TableDiff.PrimaryKey. It has always been a list — replacing a key is a remove and an add — so the singular name misread as one change.
  • The serialization types live in NSchema.Model.Serialization. They move out of NSchema.Model.Services, which is for services over the model.
  • options.AddModelConverters() composes the model’s JSON conventions. A consumer serializing NSchema types adds the converters to its own JsonSerializerOptions rather than naming them — persistence wants indented, complete output and a console stream wants terse single lines, so only the conventions are shared. The converters themselves are internal now.
  • IDatabaseIntrospector replaces ISchemaProvider. More honest about what it does now that the interface doesn’t serve both the current and desired sides, and named for what it returns.
  • Plugin Configure returns Result. Configuration errors are diagnostics like everything else.
  • Opaque SQL is SqlText now. Every schema-model field carrying SQL that NSchema stores verbatim but does not interpret is typed SqlText instead of string.
  • A qualified type’s schema is a component now. SqlType carries the schema of a user-defined type (e.g. app in app.order_status) as a structural Schema property rather than folded into its nam.
  • PolicyEnforcement absorbs the DestructiveActionPolicy enum. The shared enum gains Ignore.
  • WithDestructiveActions and WithDataHazards replace WithDestructiveActionPolicy and WithDataHazardPolicy. They set how a built-in policy is enforced, which the old names read as registering one — AddPlanPolicy<T>() is what registers.
  • The state ledger field is scripts now. Pre-5.0 executedScripts payloads read as an empty ledger. Refresh (or untaint) existing state under the state-format compatibility policy’s major-version rules.
  • Configuration is part of the language. ENGINE, PLUGIN, DATABASE and STATE parse as ordinary statements, in the one grammar that carries declarations and directives. Which statements a given file should hold is a consumer’s rule, not the parser’s: each subsystem picks out the statements it understands and ignores the rest.
  • DATABASE and STATE replace PROVIDER and BACKEND. Each names the thing it configures rather than the role that supplies it.
  • Every setting is environment-overridable. NSCHEMA_<KEYWORD>_<SETTING> overrides one setting on the matching statement — NSCHEMA_DATABASE_CONNECTION_STRING sets connection_string on DATABASE — replacing the written value or supplying one the statement omits, so a secret need not be committed. The keyword scopes the name because DATABASE and STATE may both take a setting of the same name.
  • Plugins receive PluginSettings. Configure takes the statement’s label and setting values as a flat key/value map (settings.Values, or settings.Value(name) for one), translated from the parsed statement by the configuration assembly. settings.Get<T>() binds them onto an options type — snake_case keys match properties, dotted keys nest, identifiers map to enum members — and reports an unbindable value, an unknown setting, and a failed [Required]/[Range] as error diagnostics, so a plugin declares its options rather than parsing them.
  • INSchemaDatabasePlugin and INSchemaStatePlugin replace INSchemaProviderPlugin and INSchemaBackendPlugin. Each is named for the statement that configures it.
  • Plugins are resolved by capability, not by name. INSchemaPlugin.Label is gone — the statement kind selects the capability interface, and the label in configuration is the user’s local name for a declared PLUGIN, never the plugin’s own. ScaffoldContext.Version is gone with it: the host authors the PLUGIN statement (it knows the package and the resolved version), so a plugin’s scaffold template contributes only its own configuration statement.
  • NsqlReader replaces DdlReader and diagnostics are structural. NsqlReader.Read/ReadFile return Result<NsqlDocument, NsqlDiagnostic>, the new diagnostic-typed result, with each finding carrying its source position.
  • DdlReader.Read returns Result<DdlDocument>. A syntax error is an error diagnostic instead of a thrown exception, and the parser now recovers at statement boundaries.
  • DatabaseSchema is pure data now. Filter joined Combine off the model, into the projection machinery.
  • DiffDocument.From(diff) replaces DiffReader.Read(diff). The reader type and its .Default singleton are both gone, and the namespace is NSchema.Diff.Rendering: “reader” named the wrong direction, since a DatabaseDiff is projected into display lines rather than parsed from anything.
  • SchemaScope replaces bare schema-name arrays. GetProject, GetSchema, and the plan/drift/import arguments take a scope record.
  • State locks receive complete metadata. IStateLockManager.Acquire takes AcquireLockArguments with the operation, TTL, and skip-lock; it creates the StateLockInfo atomically recorded by IStateLock.Acquire.
  • Project reads report every broken file at once. An unreadable or unparseable file (and no-files-matched) is an error diagnostic on the project.
  • Index keys and exclusion elements are column-or-expression now. IndexColumn and ExclusionElement carry mutually exclusive Column (an identifier) and Expression (verbatim SQL) properties.
  • References are value objects now. Trigger.Function carries a RoutineReference (optionally schema-qualified; unqualified resolves via the engine’s search path)
  • ObjectAddress addresses a schema-level object. Always fully qualified. Each part compares as an identifier. State and plan-file payloads serialize the address structurally.
  • Object names are SqlIdentifier now. Every name-bearing property across the schema, diff, plan, and state models carries a value object.
  • Template migrations are decoupled from their tables. Migrations can be declared in any template for any table.
  • Scripts execute as woven statements. The linearizer weaves the diff’s scripts into the ordering so scripts are now first-class actions.
  • Planning and applying now require a state store. Use the new ephemeral store if you need to run without persistent state for CI or integration tests.
  • State records what NSchema manages. Alongside the full observation and the run-once ledger, state carries the managed identity set: what an apply has created or adopted. A plan only ever covers managed/declared objects, so an object missing from the project only triggers a drop when the state contains a managed object, and objects outside the managed state are never touched.
  • Teardown destroys what NSchema manages. Plan(Empty) converges the managed set — not everything ever observed — towards nothing.
  • Managed extensions honor removal-by-absence. A declared extension becomes managed on apply and is dropped when un-declared.
  • A foreign key into an undeclared table is a warning, not an error. The target may exist unmanaged (gradual adoption), so the structural policy advises instead of blocking.
  • Constraints fold into table creates. A newly-created table’s foreign keys, unique and check constraints are now rendered inline in its CREATE TABLE rather than as trailing ALTER TABLE ADD CONSTRAINT statements.

Added

  • Enforcement that cannot be honored says so. Configuring a structural finding to be reported more leniently reports cannot-be-lowered rather than quietly doing nothing, once per finding however many times it occurs.
  • A finding says whether it may be overruled. DiagnosticKind marks a finding as Structural — NSchema cannot do what was asked, so silencing it would produce something wrong rather than something permitted — or Advisory, a judgement the caller is entitled to overrule. Enforcement can raise any finding, but can only lower an advisory one, at whatever severity it currently carries. Reporting below error severity declares a finding advisory, so a warning or an informational finding is silenceable by default and an error is not — and because the kind is fixed when the finding is created, raising one later cannot change what a caller is allowed to silence.
  • A diagnostic’s source is a DiagnosticSource. The producer’s name is typed rather than a bare string, and holds to the same shape as a code — a user groups and configures by it, so it has to be usable as a settings key too.
  • A diagnostic carries a code. Diagnostic.Code names a finding independently of how it is worded, so a message can be reworded without breaking anything that refers to it. A code is restricted to hyphen-separated lowercase words, so it is usable as a settings key, and an invalid one is rejected rather than rewritten. Every code is unique across NSchema — the producer is not always known at compile time, so the code alone addresses a finding.
  • A diagnostic collection folds to a result. ToResult() and ToResult(value) move onto DiagnosticCollection<TDiagnostic>, so accumulating findings and returning them needs no collector. A collection can also be built from a collection expression.
  • A typed result lifts like a plain one. Result<TValue, TDiagnostic> converts implicitly from a value and from a single diagnostic, and a value-less Result converts from a diagnostic — so a method returns one directly instead of naming the result type to construct it.
  • SqlDialect.CanAlterForeignKeys. A dialect states whether a foreign key can be added to, or dropped from, a table that already exists. One that cannot (SQLite) keeps every key on the CREATE TABLE that declares it, and the plan never separates one from its table.
  • The comparison seam is the SqlEquivalence class. A provider can register (UseSqlEquivalence<T>()) one equality comparer per comparison context, deciding when two spellings mean the same thing. The neutral base compares types structurally and defaults on cosmetics-normalized text.
  • Default expressions are SqlDefaultExpression now. Column.DefaultExpression and DomainType.Default graduate from SqlText to their own value object, so equivalence rules register against the specific context.
  • Change-script targets are value objects. ChangeScript now takes a ChangeTarget instead of separate trigger and path values.
  • Atomic table-fragment merge. Table.TryMergeMembers applies a complete table-member fragment or reports its conflicts.
  • SQL type conversion risk. SqlType.ConversionRiskTo assesses whether converting stored values can fail.
  • Ephemeral state. UseEphemeralState() registers an in-memory state store, which serves as the lock too, intended for disposable databases.
  • Object-granular targeting. PlanningScope covers a single list of Addresses (PlanningScope.To(addresses) / scope.Addresses), mixing whole-schema and object-level targets.
  • Address parsing. NsqlReader.ReadAddress parses a schema, schema.object, or schema.object.member fragment into an Address, resolving quoted segments
  • Address containment. Address.Covers(other) expresses downward containment (a schema covers its objects and members; an object covers its members).
  • Scope and identity are address-based. PlanningScope is scoped when it holds any address (schema or object).
  • ObjectAddress carries an optional Kind. A null kind addresses every kind sharing the name (kind-free targeting); a set kind disambiguates same-named objects.
  • Token.QuotedIdentifier. Synthesizes a quoted-identifier token (decoded text, quoted-and-escaped raw), the counterpart to Token.StringLiteral.
  • Configuration statements are built by factory. SettingsStatement.Database(label) / .State(label) / .Plugin(label) / .Engine() / .Lock() replace the constructor, refined with WithSetting(key, value), WithDocComment(text) and WithLeadingComment(text).
  • A plugin declares its scaffolding questions. INSchemaPlugin.GetScaffoldPrompts(context) returns the ScaffoldPrompts a front-end should put to the user, and the answers arrive on ScaffoldContext.Answers for GetScaffoldTemplate to build its statement from.
  • PLUGIN declares plugin dependencies. PLUGIN <label> ( source = '…', version = '…' ); separates dependency declaration from configuration.
  • ENGINE asserts the engine and/or host version. ENGINE ( version = '…', host_version = '…' );version is checked against the engine (Core), host_version against the host tool.
  • The engine handshake. PluginHandshake.Validate checks a loaded plugin assembly against the hosting engine before any of its types are instantiated.
  • ConfigurationProvider.Load loads a configuration. One call from ordered configuration layers (a later layer overrides an earlier one) to a validated ConfigurationDefinition.
  • The plugin lockfile. LockFileManager reads and writes nschema.lock (a LOCK ( source = '…', version = '…' ); grammar) as a LockFile of LockedPlugin pins. LockFile.Resolve(declaration) resolves a declaration against the lock.
  • VersionRange.IsExact / ExactVersion / Highest. Report whether a range pins a single version and the version it pins, and select the highest of a supplied set of versions that the range admits.
  • Plugins split from configuration. The provider interfaces stay in NSchema.Plugins (INSchemaPlugin and friends, the handshake, ScaffoldContext); everything a project declares, now lives under NSchema.Configuration. PluginSettings is in NSchema.Configuration.Plugins.

Removed

  • Address.SchemaName. An address answers where it sits through its own components.
  • PRE|POST DEPLOYMENT '<name>' AS $$…$$; and MIGRATION ['<name>'] FOR <event> <path> AS $$…$$; no longer parse.
  • The DROP statements (DROP SCHEMA|TABLE|VIEW|ENUM|DOMAIN|TYPE|SEQUENCE|FUNCTION|PROCEDURE|ROUTINE|EXTENSION) no longer parse. Remove the declaration instead.
  • CREATE PARTIAL SCHEMA no longer parses. The managed identity set covers what it was reaching for: an object the state does not record as managed is never dropped, declared or not.
  • RENAMED FROM clauses no longer parse. A rename is a RENAME directive now (see Management directives above).
  • The NSCHEMA configuration statement no longer parses.
  • DataMigration has been folded into Script and now requires a name for so they can maintain a stable identity.
  • Narrowed public surface. A variety of types that should never have been exposed have been made internal.
  • PolicyDiagnostics, PluginConfigureResult, and the DestructiveActionPolicy enum — all made redundant by first-class severity on Result and the shared PolicyEnforcement.
  • DdlSyntaxException, PlanFileDeserializationException, and StateDeserializationException are now internal; the read seams surface these failures as diagnostics.
  • MigrationAction.IsDestructive has been removed. Destructiveness is judged from the diff by DestructiveActionPolicy, not per action.
  • ViewDiff.DependsOn has been removed.

Fixed

  • Tables are created and dropped in foreign-key order. A table is now always created after the tables it references and dropped before them, across schemas, so a teardown no longer fails on a table another one still points at. Where tables point at each other no order can satisfy every key, so the ones it cannot are moved out of the way: on a drop the constraint goes first, and on a create it is added once both tables exist rather than riding the CREATE TABLE. A dialect that cannot alter foreign keys says so with SqlDialect.CanAlterForeignKeys, and keeps every key on the table that declares it.
  • Schemas are no longer created implicitly. Creating an object without also creating its schema will no-longer auto-adopt the parent schema.
  • Adoption-only plans now count as non-empty. Running a plan that changes nothing, and only imports objects into the managed set now correctly reports as having changed.

5.0.1

Fixed

  • Policy enforcement ignored on apply/destroy. Running a plan or apply will now correctly respect policy enforcement adjustments, so destructive actions can be made allowed.

5.1.0

Changed

  • Schema.IsImplicit is now part of the provider contract. Each introspector should set it to report the schemas its engine provides. It’s also now recorded in the state payload.

5.2.0

Added

  • Type references resolve against the database. A plan now verifies that every type the project references will exist once it applies.
  • The type vocabulary is part of the provider contract. An introspector can capture the types the engine and its extensions provide (NativeType) and record which extension provides an object (ProvidedBy).
  • Extensions now include their types. A plan that drops an extension now blocks when data still depends on its types.
  • Types are a category. TypeObject is the common base of everything a type reference can resolve to: EnumType, DomainType, CompositeType, and now NativeType.

Changed

  • IsImplicit and ProvidedBy live on every database element. An implicit object, like a container-only schema, or a native type, is never created, dropped, managed, or imported, whatever its kind.

Fixed

  • Schema-qualified engine types no longer block planning. A reference like pg_catalog.tsvector on an imported column previously demanded a declaration nobody could write.

5.3.0

Changed

  • A create is a create; a replacement says so. A definition-only change to a routine or view now emits the new ReplaceRoutine/ReplaceView actions instead of reusing CreateRoutine/CreateView.
  • Routines are created after the tables they may reference. Routine actions now order after table creation and before constraints, triggers, and views.

5.4.0

Added

  • Aggregates. CREATE AGGREGATE name(args) (…) is now supported as a kind of routine.
  • Routine actions include signatures. Routine-based actions now include the routine signature in their arguments for engines that require a signature to drop a routine.

Changed

  • Routines are created in dependency order. A routine’s definition is scanned at projection for the objects it references — the tables and views its queries read, and the routines it calls — and creates are ordered so a callee precedes its caller, across schemas. The scan is deliberately shallow and errs wide: a reference only matters when it names an object in the same plan.

Fixed

  • Teardown mirrors creation. Routines now drop before the tables they may reference, an aggregate drops before the functions it is assembled from, and a dropped table sheds its triggers explicitly first.

5.5.0

While this is technically a bug fix, it does also make additive changes to the state file in the form of capturing every opaque SQL body twice, one as-written in the project, and again as the database stores it.

Fixed

  • Hand-authored definitions now don’t cause drift. For objects that store a provider-specific body (triggers, routines, views, etc.), the state now records both the project-side and database-side definitions for comparisons. Planning does its best to compare the different versions symmetrically, so the two being written differently doesn’t show as drift.

5.6.0

Added

  • Models answer their own dependencies. View.Reads(schema), Routine.References(schema), Column.References(schema), CheckConstraint.References(schema), and Trigger.References(schema) (its function reference plus its scanned body and WHEN predicate).
  • SqlLexer: one lexical layer for engine SQL. A public, tolerant scanner over the amorphous “SQL grammar” — nestable block comments, '…' strings, "…" and […] identifiers with doubling escapes, etc.

Changed

  • The whole migration is linearized from the dependency graph. Action ordering is a priority-respecting topological sort: every create runs after what it requires and every drop before what requires it.

Fixed

  • Multi-statement bodies round-trip. A routine definition or view body may now be written as a single dollar-quoted block ($$ … $$), the same quoting triggers and scripts already use, and import writes that form whenever the bare text would not re-parse. A T-SQL body’s top-level ; previously ended the statement early, so an imported SQL Server routine could not be read back.
  • Trailing line comments no longer swallow what follows. import guards a routine definition or argument list that ends in a -- comment, which previously commented out the closing ) or ; the writer printed on the same line.

5.6.1

Added

  • Lockfiles can be updated additively. LockFile.With(pins) applies a resolution over the existing file, replacing the entry for each source it names and appending the rest, so partial resolutions don’t clobber other pinned versions.

Fixed

  • Multi-line doc comments indent correctly. Every line of a --- doc comment on a column or setting now takes the member indent, rather than only the first.
  • A state payload with no captured schema is rejected. Reading one now fails as an unreadable payload, instead of throwing an NRE.

5.7.0

Added

  • Clustered indexes. TableIndex.Clustered, PrimaryKey.Clustered and UniqueConstraint.Clustered record whether a relation’s rows are physically ordered by that index, written as T-SQL writes it.
  • A relation may declare only one clustered index. multiple-clustered-indexes is an error: a clustered index is the relation’s row order, so a second one has nothing left to order.
  • Clustering an engine cannot honor is reported rather than dropped. SqlDialect.SupportsClustering says whether the engine has clustered indexes at all.
  • XML schema collections. Schema.XmlSchemaCollections holds the named XSD bundles a typed xml column is validated against, written as CREATE XML SCHEMA COLLECTION s.name AS '…' and bound from a column as xml(DOCUMENT s.name).
  • XML indexes. TableIndex.Xml carries an XmlIndexDefinition — the kind (primary, path, value, property) and, for a secondary, the primary XML index whose node table it reads. Written as SQL Server writes it.
  • Views declare their schema binding. View.IsSchemaBound records whether a view is bound to the schema of what it reads, written in NSQL as CREATE VIEW name WITH SCHEMABINDING AS …, the spelling SQL Server uses.

Changed

  • An unscoped run stays unscoped. It no longer derives a schema list from the project and the recorded state. What a run may touch is the managed set’s job.
  • A view may carry indexes whether or not it is materialized. CREATE INDEX … ON schema.view no longer requires the view to be materialized.
  • CreateIndex says whether its owner is a view. The new OnView flag rides the action, as IsMaterialized does on the view actions.

Fixed

  • Objects managed inside an unmanaged schema are no longer stranded. An object can be managed in a schema that is not.
  • A comment opening an opaque body survives the round trip. A view body or routine definition whose first line is a comment lost it on re-parse. This is now fixed.
  • A view carrying indexes is recreated, not replaced. Indexes hang off a view’s stored form, and SQL Server’s CREATE OR ALTER VIEW drops an indexed view’s indexes outright, so redeclaring one in place destroyed them silently. A body or binding change on a view carrying indexes now drops and recreates it, rebuilding the indexes with the definition.

5.8.0

Added

  • A plugin may be declared by path. PLUGIN db ( path = './artifacts/NSchema.Postgres.dll' ) names a built assembly instead of a package to resolve, so a provider can be exercised straight from dotnet build output with no packing, feed or restore.

Changed

  • LockFile.Resolve takes a PackageReference rather than a PluginDeclaration. A plugin declared by path has no version to resolve and no lockfile entry, so it is now unable to reach this rather than guarded inside it.
  • A PLUGIN statement with no attributes reports one finding, not two. It used to report source and version as separately missing; with path as an alternative neither is required on its own, so it reports missing-plugin-origin instead.

5.9.0

Added

  • RESTRICT is a referential action in its own right. ReferentialAction.Restrict is parsed, written and rendered.
  • SqlDialect.SupportsRestrict says whether the engine has RESTRICT as distinct from NO ACTION. Providers have to opt in.
  • A generated column records whether it is stored. Column.IsStored and the NSQL STORED / VIRTUAL keywords.
  • SqlDialect.SupportsVirtualGeneratedColumns says whether the engine can leave one unstored. Off by default, matching Postgres, which stores every generated column.
  • virtual-generated-column-not-supported is a warning: a virtual generated column declared against an engine that always stores.
  • A column records whether it is the table’s row identifier for merge replication. Column.IsRowGuid and the NSQL ROWGUIDCOL keyword.
  • A column default can carry its constraint’s name. Column.DefaultConstraintName and the NSQL CONSTRAINT <name> DEFAULT <expr> form, for engines that make a default a named constraint.
  • An identity and a trigger can stand aside for a replication agent. IdentityOptions.NotForReplication, Trigger.IsNotForReplication, and the NSQL NOT FOR REPLICATION form on both.
  • DiagnosticSources declares every source the engine reports under, and DiagnosticSources.All offers them to a caller checking a configured name against the sources that exist.

Changed

  • Diagnostic sources and codes are a configuration contract, so this release settles their naming before the next CLI makes them addressable from .editorconfig.

Fixed

  • Trigger.Clone(), Equals and GetHashCode cover every field, as Column’s now do. Both enumerate fields by hand, and import clones every object on its way to a file.

5.9.1

Added

  • Statements contain their originating action type. SqlStatement.Action names the MigrationAction a statement came from, and is included in the plan file.

Fixed

  • Invalid plan files now throw immediately. The JSON held in a plan file is now more strictly deserialized so as to catch an invalid payload earlier.

5.9.2

Fixed

  • SqlStatement keeps the constructor providers were built against. 5.9.1 added the action as a positional parameter, which deletes the arity every already-compiled provider calls.

5.9.3

Fixed

  • Authored check constraints settle. Check expressions are opaque SQL, rewritten by the engine, so a handwritten constraint would never match. Both definitions are now kept and declared like-for-like as they are for triggers, and routines.

5.10.0

Added

  • SqlDialect.SupportsComments says whether the engine records a comment at all. True unless a dialect says otherwise, which is the opposite of the other capabilities: they describe something extra an engine might do, while this describes something nearly every engine does, so a provider built before the flag has to keep meaning yes.
  • comments-not-supported is a warning: documentation declared against an engine that records none, naming every object it covers in one finding rather than one per action skipped while rendering.

5.10.1

Fixed

  • Every remaining authored expression settles. Column defaults and generated expressions, index and exclusion predicates, and a domain’s checks and default are opaque SQL, rewritten by the engine, so a handwritten one would never match. All are now kept and declared like-for-like as they are for triggers, routines etc. An expression the database no longer reports is still drift, not a spelling to restore.
  • Renaming a type no longer retypes the columns declared against it. The rename moved the type but left every reference naming the old one, so each column read as a retype.
  • Recreate is now correctly blocked by dependents. Recreating a type that’s in use now causes an error.

5.10.2

Fixed

  • An identity that explicitly declares no options no longer differs from one that does so implicitly. No options at all and a set of unstated ones now compare equal.
  • A sequence altered for one reason no longer restates the others. The change carries the folded options, so a plan that changes the cache does not also restate a start it never asked to change.
  • Identity and sequence restarts now warn correctly. Restarts are data hazards: restarting the counter, means duplicate values are issued, meaning inserts collide with what is already stored.

5.11.0 Latest

Added

  • .nsql extension. NSchema files can now be authored from a .nsql extension as well as a .sql extension.
  • TextMate Grammar. There is a TextMate grammar project in the grammar directory in this project that will enable syntax highlighting for .nsql files.

Changed

  • Default file extension. The default file extension is now .nsql.