CreateTable can create filtered and covering indexes, generated columns, enum check constraints and comments from
attributes on your Data Models, which otherwise need a hand-written [PostCreateTable] or a migration:
| Attribute | Creates |
|---|---|
[Index(Where = "...")] |
A filtered (partial) index of the rows matching a condition |
[CompositeIndex(..., Include = [...])] |
A covering index that also keeps other columns |
[CompositeIndex("TenantId", "CreatedDate DESC")] |
An index with descending columns |
[Compute("...")] |
A column whose value the RDBMS generates |
[CheckEnum] |
A check constraint that only allows an Enum's values |
[Description("...")] |
A comment on a table or column |
INFO
Attributes are used when a table is created. Changing one on a table that already exists doesn't change the table, use a migration to change existing tables. Schema Diff finds the indexes and check constraints that are different to the model's and writes the migration for you, but doesn't compare the expressions of generated columns or comments.
Referencing columns​
SQL in these attributes can reference columns by their property name in braces, which are replaced with the quoted column name of each RDBMS, including its naming convention:
[Index(Where = "{DeletedDate} IS NULL")] // "DeletedDate" IS NULL, or "deleted_date" IS NULL in PostgreSQL
Filtered indexes​
A filtered index only contains the rows matching its Where condition. A unique one only applies to those rows, e.g.
an email can be reused once its subscriber is deleted:
public class Subscriber
{
[AutoIncrement]
public int Id { get; set; }
[Index(Unique = true, Where = "{DeletedDate} IS NULL")]
public string Email { get; set; }
public DateTime? DeletedDate { get; set; }
}
CREATE UNIQUE INDEX uidx_subscriber_email ON "Subscriber" ("Email") WHERE "DeletedDate" IS NULL;
They're also smaller and faster than indexing every row when queries only use some, e.g. the orders that haven't
shipped. [CompositeIndex] has the same Where:
[CompositeIndex(nameof(TenantId), nameof(Email), Unique = true, Where = "{DeletedDate} IS NULL")]
| RDBMS | Filtered indexes |
|---|---|
| PostgreSQL, SQL Server, SQLite | Supported |
| MySQL, MariaDB | NotSupportedException when the table is created |
Covering indexes​
Include keeps other columns with an index, so a query that only uses them and the indexed columns is answered from
the index without reading the table:
[CompositeIndex(nameof(TenantId), nameof(Status), Include = [nameof(Name), nameof(Size)])]
public class Upload
{
[AutoIncrement]
public int Id { get; set; }
public int TenantId { get; set; }
public string Status { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public DateTime CreatedDate { get; set; }
}
// Answered from the index
var sizes = db.Column<long>(db.From<Upload>()
.Where(x => x.TenantId == tenantId && x.Status == "Ready")
.Select(x => x.Size));
| RDBMS | Index |
|---|---|
| PostgreSQL 11+, SQL Server | ("TenantId", "Status") INCLUDE ("Name", "Size") |
| SQLite, MySQL, MariaDB | ("TenantId", "Status", "Name", "Size"), which covers the same queries |
The columns of a unique index aren't changed on SQLite and MySQL, as more key columns would change which rows it
allows. [Index] on a property has the same Include.
Descending index columns​
Add DESC to a column of a [CompositeIndex] to match a newest first sort:
[CompositeIndex(nameof(TenantId), "CreatedDate DESC")]
public class Upload { ... }
Generated columns​
A [Compute] column with an expression is calculated by the RDBMS. With [Persisted] it's stored with the row and
kept up to date whenever the columns it uses change, otherwise it's calculated when it's read:
public class LineItem
{
[AutoIncrement]
public int Id { get; set; }
public int Quantity { get; set; }
public int UnitPrice { get; set; }
[Compute("{Quantity} * {UnitPrice}"), Persisted]
public int Total { get; set; }
}
var id = db.Insert(new LineItem { Quantity = 3, UnitPrice = 20 }, selectIdentity: true);
db.SingleById<LineItem>(id).Total; //= 60
db.UpdateOnly(() => new LineItem { Quantity = 5 }, where: x => x.Id == id);
db.SingleById<LineItem>(id).Total; //= 100
// Query and index them like any other column
var large = db.Select<LineItem>(x => x.Total > 100);
Generated columns are never inserted or updated, any value they're given is ignored.
| RDBMS | [Compute("...")] |
[Compute("..."), Persisted] |
|---|---|---|
| SQL Server | AS (...) |
AS (...) PERSISTED |
| MySQL, MariaDB, SQLite | GENERATED ALWAYS AS (...) VIRTUAL |
GENERATED ALWAYS AS (...) STORED |
| PostgreSQL | GENERATED ALWAYS AS (...) STORED |
GENERATED ALWAYS AS (...) STORED |
PostgreSQL only has virtual generated columns from v18, so they're always stored.
INFO
[Compute] without an expression is unchanged: it's for a column that already exists in the table, which OrmLite
reads but doesn't create or write to. [Compute, Persisted] without an expression is a regular column.
Enum check constraints​
[CheckEnum] only allows the values of a property's Enum, so the database rejects any other value written by another
App or by raw SQL:
public enum ShipmentStatus { Packed, Shipped, Delivered }
[EnumAsInt]
public enum ShipmentPriority { Standard = 1, Express = 2 }
public class Shipment
{
[AutoIncrement]
public int Id { get; set; }
[CheckEnum]
public ShipmentStatus Status { get; set; }
[CheckEnum]
public ShipmentPriority Priority { get; set; }
}
CONSTRAINT CHK__Shipment_Status CHECK ("Status" IN ('Packed','Shipped','Delivered')),
CONSTRAINT CHK__Shipment_Priority CHECK ("Priority" IN (1,2))
The values are those the Enum is stored with: its names by default, or its numbers with [EnumAsInt]. A nullable Enum
also allows NULL. It's combined with a [CheckConstraint] on the same property.
WARNING
Adding a value to the Enum needs a migration to replace the constraint of existing tables,
until then the database rejects the new value. Schema Diff reports the constraint as changed
and writes the migration that replaces it. [Flags] Enums aren't supported as their values are combined.
Comments​
[Description] on a Data Model and its properties is added to the table and its columns, where it's shown by
database tools:
[Description("Books that customers can order")]
public class CatalogBook
{
[AutoIncrement]
public int Id { get; set; }
[Description("The book's title, as it's printed")]
public string Title { get; set; }
}
| RDBMS | Comments |
|---|---|
| PostgreSQL | COMMENT ON TABLE and COMMENT ON COLUMN |
| MySQL, MariaDB | COMMENT on the table and its columns |
| SQL Server | MS_Description extended properties |
| SQLite | Ignored, as it has no comments |
Other RDBMS​
These attributes are supported by PostgreSQL, SQL Server, MySQL, MariaDB and SQLite. Use Pre / Post Custom SQL Hooks for anything else your RDBMS supports.