navree
Documentation

Indexes and foreign keys

Primary keys, unique, plain, partial, expression, full-text and spatial indexes with their options per database, foreign keys with their actions, check constraints, conventional names, and dropping and renaming them.

On this page

Indexes make queries fast and keep values unique; foreign keys keep references between tables valid. Both are declared on the blueprint, in schema.create or schema.table, next to the columns they cover.

Rust

Indexes

An index is declared on a column, or on the blueprint for several columns:

Rust
MethodCreates
t.primary(columns), .primary()The primary key (one per table).
t.unique(columns), .unique(), .unique_named(name)A unique index (a unique constraint on PostgreSQL).
t.index(columns), .index(), .index_named(name)A plain index.
t.fulltext(columns), .fulltext()A full-text index (below).
t.spatial_index(columns), .spatial_index()A spatial index.
t.raw_index(expression, name)An index on an expression, written as SQL.

columns is one name ("email") or an array (["workspace_id", "email"]). Column indexes are created after the table and in the order declared; in table, every command runs in order, so t.string("slug") followed by t.unique(["slug"]) adds the column first.

Index options

The blueprint methods return an index builder:

OptionDoes
.name("members_email")Names the index instead of the conventional name.
.algorithm("gin")The index method, written as USING ..: btree, hash, gin, gist, brin on PostgreSQL.
.operator_class("text_pattern_ops")A PostgreSQL operator class (text_pattern_ops for prefix LIKE, vector_cosine_ops for vectors).
.where_raw("deleted_at IS NULL")A partial index over the matching rows (PostgreSQL and SQLite; MySQL refuses it).
.nulls_not_distinct()A unique index where NULLs are equal, so at most one row may be NULL (PostgreSQL 15 and later).
.online()CREATE INDEX CONCURRENTLY, which doesn't block writes (PostgreSQL; run the migration outside a transaction).
.deferrable(), .initially_deferred(), .initially_immediate()A deferrable unique constraint, checked at commit (PostgreSQL).
.lock("none"), .inplace()MySQL's DDL lock mode and ALGORITHM=INPLACE.
.language("english")The text-search configuration of a PostgreSQL full-text index.

A partial unique index lets soft-deleted rows keep their values without blocking new ones: t.unique(["email"]).where_raw("deleted_at IS NULL") creates CREATE UNIQUE INDEX "users_email_unique" ON "users" ("email") WHERE deleted_at IS NULL.

Full-text and spatial indexes

Rust

A full-text index is GIN over to_tsvector on PostgreSQL, so the ORM's where_full_text uses it, and FULLTEXT on MySQL. SQLite has no full-text index: the builder refuses it and points to an FTS5 virtual table. Spatial indexes are GIST on PostGIS columns and SPATIAL on MySQL.

Names

Indexes are named {table}_{columns}_{kind}, with primary, unique, index, fulltext, spatialindex or foreign as the kind, lower-cased, with - and . replaced by _: deals_stage_created_at_index, members_workspace_id_email_unique, deals_company_id_foreign. A name longer than 63 bytes (PostgreSQL's limit) is shortened to 54 bytes plus _ and an 8-digit hash, the same way on every database, so a drop by columns finds it. Schema::index_name(table, kind, columns) computes one.

Foreign keys

A foreign key column usually references the conventional table, by constrained():

Rust

constrained() takes the table from the column's name, company_id referencing companies.id (the part before _id, plural). constrained_on(table, column) names both. t.foreign(columns).references(columns).on(table) declares one on the blueprint, for any columns, composite keys included.

OptionDoes
.cascade_on_delete()Deletes the referencing rows with the referenced row.
.restrict_on_delete()Refuses to delete a referenced row.
.null_on_delete()Sets the referencing column to NULL (make the column nullable).
.no_action_on_delete()ON DELETE NO ACTION, the SQL default.
.cascade_on_update(), .restrict_on_update(), .null_on_update(), .no_action_on_update()The same, when the referenced key changes.
.on_delete(ReferentialAction::SetDefault), .on_update(..)Any action, as a value.
.name("deals_company_fk")Names the constraint instead of {table}_{columns}_foreign.
.deferrable(), .initially_deferred()A constraint checked at commit (PostgreSQL).
.not_valid()Added without checking existing rows (PostgreSQL; validate later with ALTER TABLE .. VALIDATE CONSTRAINT).
PostgreSQL

A foreign key needs the referenced table, so order the migration sets: a deals set that references companies runs .after(["companies"]), and a table referencing users runs after "auth". On SQLite, foreign keys are part of the table, and adding or dropping one later rebuilds it. Navree's time-series tables are hypertables on TimescaleDB whose keys include the time column, so nothing may reference their id alone (TimescaleDB).

Foreign key violations answer 409 over HTTP, like unique violations (database errors).

Check constraints

A check constraint is a rule every row must meet, enforced by the database whatever writes the row. t.check(name, expression) adds one; the expression is SQL and may name several columns:

Rust
PostgreSQL

PostgreSQL, MySQL and MariaDB add the constraint with ALTER TABLE, in schema.create and schema.table alike. SQLite writes it into CREATE TABLE; adding one to an existing table, or dropping one, rebuilds the table with its rows. Adding a check that existing rows break fails, and so does a write that breaks one. MySQL parses checks before 8.0.16 without enforcing them, and the migration says so in a notice. Name each check after its table: MySQL wants constraint names unique in the whole database. Shared libraries use one to keep their owner columns consistent.

Dropping and renaming

Rust

Drops take a name or the columns, which give the conventional name: drop_index, drop_unique, drop_fulltext, drop_spatial_index, drop_foreign, drop_primary() (the conventional primary key) and drop_primary_named(name). drop_constrained_foreign_id drops a foreign key and then its column. drop_check takes the check's name. To check before dropping, schema.has_index(table, name_or_columns, kind) and schema.has_foreign_key(table, ..) read the live schema.