Concepts & pipeline
This page goes over the core concepts in NSchema in more detail.
Domain model
Section titled “Domain model”NSchema works around a simple domain model of schemas, tables, columns, indexes, constraints, and views. The model is designed to be flexible enough to represent the features of any relational database, while still being simple and intuitive to work with.
These models are used to represent both the desired state (what you want) and the current state (what the database has), so they can be compared symmetrically and transformed as needed.
Because they’re just .NET objects, the schema can be constructed in any way you like. The usual source is a SQL DDL file (see Defining schemas), but you could just as easily generate it from code or use an existing provider to pull it from the database itself.
Pipeline
Section titled “Pipeline”This section goes over the high-level pipeline steps that every NSchema application flows through. Most stages have an interface you can swap or extend; these are covered in more detail below and in Extension points.
Planning
Section titled “Planning”This section of the pipeline is where the migration plan is generated, by the Plan operation. It never writes to the
database, so a plan can always be computed and previewed first; an Apply then executes a plan produced here (a fresh one
or a saved one).
- Read the desired schema. The SQL files in your project directory are composed into a single view of your goal schema.
- Validate the desired schema. Checks are done to make sure the schema is valid: primary keys, referential integrity, etc.
- Read the current schema. The current schema is introspected from the target database’s metadata tables.
- Diff the schemas. The schemas are compared and output as a hierarchical diff.
- Validate the diff. Checks the plan against configured policies (for example, the built-in guard against destructive changes).
- Linearize the diff. The complex diff is reduced to a dependency-ordered list of actions (create table, add index, etc.).
- Generate SQL. The action list is handed off to a database-specific provider to generate the required SQL.
Applying
Section titled “Applying”This section runs for an Apply operation. It takes a compiled plan’s SQL, handed to ApplyArguments.Sql, and executes it against the database.
- Execute the migration. Takes the SQL plan (from a
Planoperation or a saved plan file) and executes it against the target. - State capture. After a successful apply, the resulting schema is captured to the state store (if configured) so that future plans can be generated against it.
Refresh
Section titled “Refresh”The Refresh operation captures the current live schema to the state store without doing any planning or applying. This
is useful for recording drift that happened between applies, or for initializing the state store with the current schema.
Schemas: desired and current
Section titled “Schemas: desired and current”The desired schema comes from the SQL DDL files registered with AddDdlSchemas(...). That method may be called more
than once (for example a base set plus an environment overlay) and the sources are aggregated into a single schema before
planning, so you can freely split a schema across many files and directories.
The current schema has two possible sources, selected automatically per operation:
- Online. The live database, read through an
ISchemaProviderregistered viaUseCurrentSchema<T>()or a provider package likeUseCurrentSchemaPostgres(...). - Offline. A persisted snapshot, available when an
ISchemaStateStoreis registered viaUseStateStore<T>()/UseFileStateStore(...).
Which source a Plan reads is chosen by its PlanArguments.Target: a preview (Recorded, the default) prefers the offline snapshot when one is available so planning works without a database connection, while planning for an apply (Live) always reads the live database.
Schema scope
Section titled “Schema scope”By default, a run’s scope is the full set of schemas declared in your desired DDL. You can narrow it per run with the
Schemas argument on the operation (e.g. app.Operations.Plan(new PlanArguments { Schemas = ["app"] })). The live ISchemaProvider
is then asked only for those schemas. GetSchema(...) takes the names to read and returns everything it describes when
none are given.
Schema policies
Section titled “Schema policies”Schema policies are used to validate the desired schema before any comparison or planning is done. This is where you can enforce naming conventions, required columns, banned types, or any other rules you want to apply to your schema.
Schema policies are implemented using ISchemaPolicy and are registered with AddSchemaPolicy<T>(). If the policy
returns any errors, execution will halt, preventing bad schemas from being applied.
Diff policies
Section titled “Diff policies”Diff policies validate the structured diff between the current and desired schema before a plan is built. This is where you enforce rules about what kinds of changes are allowed, for example preventing destructive actions like dropping tables or columns (the built-in destructive-action policy lives here).
Diff policies are implemented using IDiffPolicy and registered with AddDiffPolicy<T>(). If any policy returns errors,
execution will halt, preventing bad changes from being applied.
SQL generation and execution
Section titled “SQL generation and execution”Once the plan is validated, NSchema turns it into SQL and, for an apply, runs it. These are two separate steps, so a plan can be previewed without a live connection:
ISqlGeneratorturns the plan into aSqlPlan. This is pure string-building, so the SQL preview works offline; it’s supplied by a database provider likeNSchema.Postgresand registered withUseSqlGenerator<T>()(a provider package’sUseCurrentSchemaPostgres(...)wires it up for you). Only one generator is registered at a time; with none set, plans are still computed and reported, just without a SQL preview.ISqlExecutorruns theSqlPlanagainst the database, applying the configured transaction mode. It is internal, and the only online step.
Rendering output to text sits outside the DI pipeline. For the diff, the public DiffReader utility — DiffReader.Default.Read(diff),
or construct your own — projects a DatabaseDiff into a renderer-neutral DiffDocument: an ordered list of lines, each tagged with its
change kind (add/modify/remove), that a front-end folds into colour, Markdown, or plain text. Core deliberately ships no renderer for a
SqlPlan or a DatabaseSchema; turning those models into text is left to the consumer (the nschema CLI carries its own). A consumer
wanting a different format writes its own — there are no renderer interfaces or Use* registrations to satisfy.