GetSchemaDiff() compares your models with their tables in the database and returns what's in one and not the
other, so you find out that a model has changed without its table when your App starts, not when a query fails:
var diff = db.GetSchemaDiff(typeof(Invoice), typeof(Customer));
if (diff.HasChanges)
log.LogWarning("The database doesn't match its models:\n{Diff}", diff);
Invoice
~ Reference VARCHAR(50) NOT NULL -> VARCHAR(200) NULL
+ PaidDate DATETIME NULL
+ Currency VARCHAR(8000) NULL (renamed from LegacyCode?)
- LegacyCode VARCHAR(8000) NULL (not in Invoice) (renamed to Currency?)
+ index idx_invoice_customerid
Customer
+ table isn't in the database
The differences can be logged, written as a migration for you to review, or applied to the database while you're developing.
In the Admin UI​
The Database Admin UI has a Schema Diff for each database, which shows the differences between your App's models and their tables with a migration to copy into your App.
It compares these models of your App:
- The data models of your AutoQuery APIs
- The models of the tables your DB Migrations create or change
- Other models you add to the
ModelTypesof theAdminDatabaseFeature
Migrations declare their own copy of each table, as it was when the migration was written. The App's model with the
same table name, from its [Alias] or class name, is the latest version of the table, which is the one that's
compared. Tables whose name is used by more than one of your App's models are only compared when one of them is the
data model of an AutoQuery API or in ModelTypes. Set IncludeMigrationModels = false to only compare the other
models.
The same models are available to your App with Migrator.GetMigrationTables():
var models = Migrator.GetMigrationTables([typeof(Migration1000).Assembly], [typeof(Invoice).Assembly])
.Where(x => x.ModelType != null)
.Select(x => x.ModelType!)
.ToArray();
var diff = db.GetSchemaDiff(models);
What's compared​
| Difference | Change | |
|---|---|---|
+ |
The table of a model isn't in the database | CreateTable |
+ |
The column of a property isn't in its table | AddColumn |
~ |
A column isn't the type or size of its property, or allows nulls when its property doesn't | AlterColumn |
- |
A column of the table isn't a property of the model | DropColumn |
+ |
An index of the model, e.g. an [Index] property, isn't in the database |
CreateIndex |
~ |
An index of the model is in the database with other columns, in another order or uniqueness | AlterIndex |
- |
An index of the table is on a column that isn't in the model, so it's dropped before its column | DropIndex |
! |
An index of the table on columns of the model isn't in the model, e.g. one added by hand. It's kept, with the attribute that declares it | IndexNotInModel |
~ |
A column's default isn't its property's [Default], or it has a default its property doesn't |
AlterDefault |
+ |
The foreign key of a [ForeignKey] or [References] property isn't in the database |
AddForeignKey |
~ |
A foreign key references another table, or has other OnDelete or OnUpdate actions |
AlterForeignKey |
- |
A foreign key of the table isn't in the model | DropForeignKey |
+ |
A [Unique], [UniqueConstraint], [CheckConstraint] or [CheckEnum] constraint isn't in the database |
AddConstraint |
~ |
A check constraint's condition isn't the model's | AlterConstraint |
- |
A unique or check constraint of the table isn't in the model | DropConstraint |
~ |
The primary key has other columns than the model's, which is reported but not changed | AlterPrimaryKey |
~ |
SQLite: the table is created again from its model to make the changes SQLite can't alter | RebuildTable |
+ |
The full-text index of a [FullTextIndex] isn't in the database |
CreateFullTextIndex |
Indexes are compared by their name, then their key columns, whether they're unique, their Include columns and their
Where condition. Databases rewrite conditions, e.g. PostgreSQL writes an IN list as = ANY (ARRAY[...]), so on
PostgreSQL and SQL Server the model's are created on a temporary table and compared as the database writes them.
Unique constraints are compared by their columns, in any order, as the database names the constraints of [Unique]
properties. Check constraints are compared by their name, then their condition as the database writes it: the model's
checks are created in a temporary table, so [CheckEnum]'s IN lists are compared however your database rewrites
them.
Indexes on columns of the model are never dropped, as they're often added to the database by hand, e.g. to speed up
a slow query. An index that isn't in the model is reported with the [Index] or [CompositeIndex] that declares
it, named like the index of the database, so adding it to the model makes the model the same as the database:
Order
! index ix_order_customer (CustomerId) (not in Order, kept: add [Index(Name = "ix_order_customer")] to Order.CustomerId)
! index ix_order_status_created (Status, CreatedDate) (not in Order, kept: add [CompositeIndex("Status", "CreatedDate", Name = "ix_order_status_created")] to Order)
Its Unique, Include columns and Where condition are included, and an index of an expression, which no attribute
declares, is kept and reported without one. MySQL's unique constraints are its unique indexes, so on MySQL they're kept
the same way, with [Unique] or [UniqueConstraint] to declare them. Only an index on a column that isn't in the
model is dropped, with DropIndex before its column.
A primary key of other columns is only reported. Changing it needs the foreign keys that reference it and its data to be migrated too, so it's left for a migration you write. Until then, the columns of the primary key in the database aren't altered.
Default values are compared as your database writes them, e.g. ((0)) on SQL Server or 'draft'::character varying
on PostgreSQL, with the defaults of the temporary table the model's columns are created in. The defaults of
[AutoIncrement] columns aren't compared, as they're their sequences.
Foreign keys are compared by their column, then the table they reference and their OnDelete and OnUpdate actions,
where RESTRICT and NO ACTION are the same:
Order
~ Quantity default DEFAULT 1 -> DEFAULT 5
~ foreign key FK_Order_Customer_CustomerId (CustomerId) REFERENCES Customer (Id) -> (CustomerId) REFERENCES Customer (Id) ON DELETE CASCADE
+ foreign key FK_Order_Customer_BillingCustomerId (BillingCustomerId) REFERENCES Customer (Id)
- foreign key FK_Order_Warehouse_WarehouseId (WarehouseId) REFERENCES Warehouse (Id) (not in Order)
Foreign keys aren't compared when OrmLiteConfig.SkipForeignKeys is set, as the model's tables are created without
them.
A column that isn't in the database and a column that isn't in the model are likely the same column renamed when
they're the only ones of their type. Their changes say so with LikelyRename, e.g:
Invoice
+ Currency VARCHAR(8000) NULL (renamed from LegacyCode?)
- LegacyCode VARCHAR(8000) NULL (not in Invoice) (renamed to Currency?)
They're only likely, so they're added and dropped as other columns, and a migration says how to rename them instead.
Columns are compared with the columns the model would be created with, as your database reports them: the properties of the model are created in a temporary table that's read and dropped. A table that's created from its model never has differences, whichever type converters and naming strategy you use.
Some types aren't a difference to each other, which are common in tables that weren't created by OrmLite, e.g. by Entity Framework:
- Text: a
TEXTcolumn, or a character column without a length likeVARCHAR(MAX), when its property would be created as aVARCHAR(n), as it holds every value of the property. SQLite doesn't use the length of a column, so its character types are all treated as the same - Integers:
SMALLINT,INTEGERandBIGINTcolumns, e.g. alongproperty with anINTEGERcolumn - SQLite decimals:
NUMERICandDECIMALcolumns of any precision and scale, which SQLite doesn't use
It's supported for SQLite, PostgreSQL, SQL Server, MySQL and MariaDB.
Ignore tables​
Tables that aren't managed by OrmLite aren't compared. The tables of ASP.NET Core Identity (AspNet*) and EF Core's
migrations (__EFMigrationsHistory) are ignored by default, including models whose [Alias] is one of their tables,
e.g. a [Alias("AspNetUsers")] model used by an AutoQuery API.
Ignore other tables by their name or model, e.g. in Configure.Db.cs:
OrmLiteConfig.SchemaDiff.IgnoreTables.Add("Legacy*"); // * matches any characters
OrmLiteConfig.SchemaDiff.IgnoreTables.Add("ImportStaging");
OrmLiteConfig.SchemaDiff.IgnoreTypes.Add(typeof(AuditArchive));
Table names are matched after [Alias] and your naming strategy, ignoring case. Remove a default to compare its
tables again, e.g. OrmLiteConfig.SchemaDiff.IgnoreTables.Remove("AspNet*").
The tables that weren't compared are in diff.Ignored, which is logged at the end of the differences, and shown
under the Schema Diff in the Admin UI:
Invoice
+ PaidDate DATETIME NULL
Ignored: AspNetUsers, AspNetRoles
To use other rules for one comparison, pass its own options:
var diff = db.GetSchemaDiff(new SchemaDiffOptions { IgnoreTables = [] }, typeof(AppUser), typeof(Invoice));
Log the differences when your App starts​
The AdminDatabaseFeature can log the Schema Diff of each database when your App starts, so differences are seen
in its startup logs, e.g. in Configure.Db.cs:
services.AddPlugin(new AdminDatabaseFeature {
LogSchemaDiff = context.HostingEnvironment.IsDevelopment(),
});
It compares the same models as the Admin UI, in the background after your App starts, and logs a warning with the differences of each database whose tables aren't the same as their models:
Schema Diff: the tables of the main database aren't the same as their models
Invoice
+ PaidDate DATETIME NULL
Ignored: AspNetUsers, AspNetRoles
Or an information message when they are. It isn't compared when running App Tasks, e.g. migrations. Comparing creates temporary tables, so it's best enabled in development.
To compare other models yourself, e.g. in a Modular Startup class:
public class ConfigureDbSchema : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureAppHost(afterAppHostInit: appHost => {
using var db = appHost.Resolve<IDbConnectionFactory>().Open();
// The models that have a table
var models = typeof(Invoice).Assembly.GetTypes()
.Where(x => x.Namespace == "MyApp.ServiceModel.Types")
.ToArray();
var diff = db.GetSchemaDiff(models);
if (diff.HasChanges)
LogManager.GetLogger(GetType()).Warn("The database doesn't match its models:\n" + diff);
});
}
Or fail a test, so it's found before it's deployed:
[Test]
public void Database_matches_its_models()
{
using var db = dbFactory.OpenDbConnection();
var diff = db.GetSchemaDiff(typeof(Invoice), typeof(Customer));
Assert.That(diff.HasChanges, Is.False, diff.ToString());
}
The changes​
Each difference is a SchemaChange, with the change that makes the database the same as the model:
foreach (var change in diff.Changes)
{
Console.WriteLine(change.Description);
Console.WriteLine(change.Sql);
}
Column Invoice.Reference is VARCHAR(50) NOT NULL in the database, VARCHAR(200) NULL in Invoice
ALTER TABLE "Invoice" ALTER COLUMN "Reference" VARCHAR(200) NULL;
Column Invoice.PaidDate isn't in the database: DATETIME NULL
ALTER TABLE "Invoice" ADD "PaidDate" DATETIME NULL;
Column Invoice.LegacyCode isn't in Invoice: VARCHAR(8000) NULL
ALTER TABLE "Invoice" DROP COLUMN "LegacyCode";
Index idx_invoice_customerid of Invoice isn't in the database
CREATE INDEX idx_invoice_customerid ON "Invoice" ("CustomerId");
| Property | |
|---|---|
Type |
CreateTable, AddColumn, AlterColumn, DropColumn, CreateIndex, AlterIndex, DropIndex, AlterDefault, AddForeignKey, AlterForeignKey, DropForeignKey, AddConstraint, AlterConstraint, DropConstraint, AlterPrimaryKey, RebuildTable, CreateFullTextIndex or IndexNotInModel |
ModelType, Table, Name |
The model, its table and the column, index, foreign key or constraint |
Field |
The property of a column, null for columns that aren't in the model |
DatabaseColumn, ModelColumn |
The column, index, default, foreign key, constraint or primary key as it is in the database, and as the model would create it |
LikelyRename |
The column that's likely the same column renamed |
Sql |
The SQL that makes the change, null if the database can't, e.g. SQLite can't alter a column, which its RebuildTable change makes |
IsRebuilt |
Whether it's made by the RebuildTable change of its table |
IsDestructive |
Whether it can lose data or fail with the rows of the table |
Attribute, AttributeTarget |
IndexNotInModel: the attribute that declares the index, and the property or model to add it to |
A change is destructive when it drops a column or an index, alters a column in any way other than making it larger or
allowing nulls, or makes an index unique. Foreign keys that aren't in the model are destructive, as they may have been
added to the database by hand. Indexes on columns of the model are never dropped, IndexNotInModel has no Sql and
isn't made by ApplySchemaDiff().
Adding a unique or check constraint is destructive, as the rows of the table can break it, and so is changing a check's condition. Constraints that aren't in the model are destructive to drop, like indexes. Adding a foreign key to a column that's in the table is destructive, as its rows can reference rows that don't exist, and so is changing the table a foreign key references. A foreign key of a column that's added in the same diff isn't, and neither are changes to defaults, which only change the rows that are inserted without a value.
Write a migration​
ToMigration() writes the changes as a DB Migration, for you to review and add to your
migrations:
var diff = db.GetSchemaDiff(typeof(Invoice));
File.WriteAllText("Migrations/Migration1005.cs", diff.ToMigration("Migration1005", "MyApp.Migrations"));
// GENERATED BY SCHEMA DIFF: A GUIDE TO REVIEW, NOT A FINISHED MIGRATION
//
// Schema Diff is new, and it's too early to know how accurate the migrations it writes are. Don't trust this
// class to do the right thing: check every change does what you intend, e.g. a column that's not in a model is
// only dropped by commented code as it may have been renamed, and run it against a copy of your data first.
// Delete the #warning below once you've reviewed it.
#warning Generated by Schema Diff: review this migration before running it, then delete this line
using System;
using System.Collections.Generic;
using ServiceStack;
using ServiceStack.DataAnnotations;
using ServiceStack.OrmLite;
namespace MyApp.Migrations;
public class Migration1005 : MigrationBase
{
public class Invoice
{
[AutoIncrement]
public int Id { get; set; }
[StringLength(200)]
public string Reference { get; set; }
public DateTime? PaidDate { get; set; }
public string Currency { get; set; }
}
public override void Up()
{
// Reference is VARCHAR(50) NOT NULL
Db.AlterColumn<Invoice>(x => x.Reference);
Db.AddColumn<Invoice>(x => x.PaidDate);
// Currency is likely LegacyCode renamed. If it is, rename it instead of adding it:
// Db.RenameColumn<Invoice>(x => x.Currency, "LegacyCode");
Db.AddColumn<Invoice>(x => x.Currency);
// LegacyCode (VARCHAR(8000) NULL) isn't in Invoice. If it was renamed, rename it instead:
// Db.RenameColumn<Invoice>("LegacyCode", "Currency");
// Db.DropColumn<Invoice>("LegacyCode");
Db.ExecuteSql("CREATE INDEX idx_invoice_customerid ON \"Invoice\" (\"CustomerId\");");
}
public override void Down()
{
Db.DropIndex<Invoice>("idx_invoice_customerid");
Db.DropColumn<Invoice>(x => x.Currency);
Db.DropColumn<Invoice>(x => x.PaidDate);
// Reference was VARCHAR(50) NOT NULL
}
}
The migration has its own copy of the model, with its Primary Key and the properties that are changed, so it keeps
working after the model changes again. Tables that aren't in the database have all their properties, and are
created with Db.CreateTable<T>().
WARNING
A generated migration is a guide, not a finished migration. Schema Diff is new, and it's too early to know how
accurate the migrations it writes are, so they start with a #warning that's reported each time your App is built,
until you've reviewed the migration and deleted it.
Review it before you run it:
- Renamed properties look like a new column and a column that isn't in the model. Columns that aren't in the model are only dropped by code that's commented out, with the rename to use instead
- Altered columns aren't reverted by
Down(), which has what the column was as a comment - New columns that don't allow nulls need a
[Default]to be added to a table that has rows - Indexes, defaults, foreign keys and constraints are changed with the SQL of the database the diff was read from
- Constraints that aren't in the model are only dropped by code that's commented out, as
DropConstraint<T>()doesn't drop constraints on every database - A primary key of other columns is a comment, for you to migrate with its foreign keys and data
- Indexes that aren't in the model are a comment with the attribute to add to the model, and are never dropped
Write it to your migrations​
The migrate.new App Task of the AdminDatabaseFeature writes the migration of your database's Schema
Diff to your App's Migrations folder, named after your last migration, e.g. Migration1005.cs:
dotnet run --AppTasks=migrate.new
It compares the same models as the Admin UI, and doesn't write anything when your database is
the same as its models, or replace a migration that's already there. Write the migration of a
named connection with its name, e.g. migrate.new:reports, which is run on its
database. The folder is the MigrationsPath of the AdminDatabaseFeature, relative to your App's content root.
Apply the changes​
ApplySchemaDiff() makes the changes to the database, and returns the changes it made:
var diff = db.GetSchemaDiff(typeof(Invoice), typeof(Customer));
// Create tables, add columns and indexes, make columns larger or allow nulls, change defaults,
// add the foreign keys of new columns and change the actions of foreign keys
var applied = db.ApplySchemaDiff(diff);
// Also drop columns that aren't in the model, and alter columns in ways that can lose data or fail
db.ApplySchemaDiff(diff, allowDestructive: true);
It's for keeping a development or test database the same as your models. Use migrations for the databases you deploy to, which are reviewed, run once and recorded.
Rebuild SQLite tables​
SQLite can't alter a column, its default, foreign keys or constraints. It changes them by
creating the table again and copying its rows, which
db.RebuildTable<T>() does from its model:
db.RebuildTable<Invoice>();
It creates the table with another name, copies the rows of the columns that are in the table and the model, drops
the table, renames the new table, then creates the model's indexes, the table's other indexes on the columns that are
kept, and its triggers again, so an index added by hand isn't lost. The next
AUTOINCREMENT id is kept, so the ids of deleted rows aren't reused. It runs in a transaction of its own, or in your
connection's transaction, e.g. a migration's, so a row that breaks the new table, e.g. its new [CheckConstraint],
leaves the table as it was.
The changes of a SQLite table's Schema Diff that SQLite can't make are made by a RebuildTable change after them, and
have IsRebuilt set:
Invoice
~ Reference VARCHAR(50) NOT NULL -> VARCHAR(200) NULL (by rebuilding the table)
~ Qty default no default -> DEFAULT 1 (by rebuilding the table)
~ rebuild table to change Reference, the default of Qty
Its migration rebuilds the table with Db.RebuildTable<Invoice>(), from the migration's copy of the model with all
its properties. Things to be aware of:
- Columns that aren't in the model aren't kept, so a rebuild is destructive when the table has them. Rename a column before the table is rebuilt to keep it
- Tables referenced by foreign keys: when foreign keys are enforced, SQLite would delete or change the rows of the
tables that reference a table that's rebuilt. Outside a transaction,
RebuildTable()stops enforcing them while it's rebuilt and checks the rows before they're committed. In a transaction, e.g. a migration's, foreign keys can only be disabled before it starts, so it throws anInvalidOperationExceptioninstead, unlessPRAGMA foreign_keys=OFFwas run before the transaction - A primary key of other columns isn't changed by a Schema Diff's rebuild, which needs the foreign keys that reference it to be migrated too
Things to be aware of​
- Not compared: the order of columns, the expressions of generated columns and comments. Oracle and Firebird don't compare indexes, defaults, foreign keys or constraints
- Temporary tables: the user of the connection needs to be able to create them, e.g. the
CREATE TEMPORARY TABLESprivilege in MySQL. When it can't, the types of columns aren't compared anddiff.Warningssays why - SQLite can't alter a column, its default, foreign keys or constraints, so their tables are
rebuilt. SQLite doesn't keep the names of foreign keys, so the ones that aren't in the model
have no
Name - Tables that aren't created from their model, e.g. by hand with an
NVARCHAR(160)column where the model creates aVARCHAR(8000), have differences for each column that isn't what the model creates, other than the text and integer types above - Changes are applied one at a time, not in a transaction, so a change that fails leaves the changes before it