Skip to content
loco
v1.0
★ 6.9k Get started

Schema & ColType DSL

loco_rs::schema is the Rails-like migration DSL migrations author against — a thin, ergonomic layer over Sea-ORM’s sea_query/SchemaManager. It is reached as loco_rs::schema::* (the generated migration template does use loco_rs::schema::*;) and re-exports all of sea_orm_migration::schema::* alongside its own additions, so lower-level column-def primitives (string, integer, pk_auto, …) are available directly if ColType doesn’t cover a case.

The module is gated behind the with-db feature (src/lib.rs:20-21).

The i64 auto primary key (1.0 default)

ColType::PkAuto builds an auto-increment 64-bit (BIGINT) primary key, not a 32-bit one:

// src/schema.rs:330-332
Self::PkAuto => big_pk_auto(name),

big_pk_auto (re-exported from sea_orm_migration::schema) is big_integer(name).auto_increment().primary_key() — a BigInteger column, i.e. Rust-side i64. This is a deliberate 1.0 change from the previous 32-bit default (comment at schema.rs:330-331: Sea-ORM 2.0 maps SQLite integers to i64, and a BIGINT PK is the modern default, matching Rails 5.1+).

The consequence propagates to foreign keys: every FK column generated by create_table/create_join_table/add_reference is typed ColType::BigInteger (or BigIntegerNull when nullable) so it matches the id column it points to (schema.rs:677-683, schema.rs:781-782). ColType::PkUuid is the alternative — a Uuid primary key with no auto-increment (pk_uuid, wraps uuid(name).primary_key()).

VariantBuildsRust typeAnchor
ColType::PkAutobig_pk_auto(name) — auto-increment BIGINT PKi64schema.rs:332
ColType::PkUuidpk_uuid(name)UUID PK, no auto-incrementUuidschema.rs:333

ColType — column type enum

enum ColType (schema.rs:161-284) is the value half of every (name, ColType) tuple passed to create_table/add_column. Each family below generally follows a modifier convention — but coverage is not uniform per family (e.g. Boolean has no *Uniq, TimestampWithTimeZone has no *Uniq, Text has no *Len):

  • (bare)NOT NULL, no default, no unique constraint.
  • *Null — nullable.
  • *UniqNOT NULL + unique index.
  • *WithDefault(v)NOT NULL + a default value.
  • *Len(n) — fixed/max length, for Char/String/Decimal(precision, scale)/binary/varbit families.

ColType::to_def(&self, name) -> ColumnDef (schema.rs:328-470) matches every variant to a Sea-ORM ColumnDef builder call; the tables below are transcribed from that match plus the enum declaration.

Primary keys — see above

Char / String / Text

VariantNotes
Char, CharNull, CharUniq, CharWithDefault(char)fixed single-char-typed column
CharLen(u32), CharLenNull(u32), CharLenUniq(u32), CharLenWithDefault(u32, char)fixed length n
String, StringNull, StringUniq, StringWithDefault(String)variable-length string, no length cap
StringLen(u32), StringLenNull(u32), StringLenUniq(u32), StringLenWithDefault(u32, String)variable-length string with max length n
Text, TextNull, TextUniq, TextWithDefault(String)unbounded text; no Len variant

Numeric — integers & unsigned

VariantRust typeNotes
Integer, IntegerNull, IntegerUniq, IntegerWithDefault(i32)i3232-bit signed
SmallInteger, SmallIntegerNull, SmallIntegerUniq, SmallIntegerWithDefault(i16)i16
BigInteger, BigIntegerNull, BigIntegerUniq, BigIntegerWithDefault(i64)i64also the type auto-generated for FK columns
Unsigned, UnsignedNull, UnsignedUniq, UnsignedWithDefault(u32)u32
SmallUnsigned, SmallUnsignedNull, SmallUnsignedUniq, SmallUnsignedWithDefault(u16)u16
BigUnsigned, BigUnsignedNull, BigUnsignedUniq, BigUnsignedWithDefault(u64)u64

Numeric — decimal / float / money

VariantNotes
Decimal, DecimalNull, DecimalUniq, DecimalWithDefault(f64)unconstrained precision
DecimalLen(u32, u32), DecimalLenNull(u32, u32), DecimalLenUniq(u32, u32), DecimalLenWithDefault(u32, u32, f64)(precision, scale)
Float, FloatNull, FloatUniq, FloatWithDefault(f32)f32
Double, DoubleNull, DoubleUniq, DoubleWithDefault(f64)f64
Money, MoneyNull, MoneyUniq, MoneyWithDefault(f64)currency-typed column

Boolean

VariantNotes
Boolean, BooleanNull, BooleanWithDefault(bool)no *Uniq variant

Date / time

VariantNotes
Date, DateNull, DateUniq, DateWithDefault(String)
Time, TimeNull, TimeUniq, TimeWithDefault(String)
DateTime, DateTimeNull, DateTimeUniq, DateTimeWithDefault(String)naive datetime, no timezone
TimestampWithTimeZone, TimestampWithTimeZoneNull, TimestampWithTimeZoneWithDefault(String)timezone-aware; no *Uniq variant
Interval(Option<PgInterval>, Option<u32>), IntervalNull(..), IntervalUniq(..)Postgres interval; args are an optional PgInterval field-qualifier and optional precision

Binary

VariantNotes
Binary, BinaryNull, BinaryUniqunbounded, no default variant
BinaryLen(u32), BinaryLenNull(u32), BinaryLenUniq(u32)fixed length n
VarBinary(u32), VarBinaryNull(u32), VarBinaryUniq(u32)variable, max length n
Blob, BlobNull, BlobUniq

JSON

VariantNotes
Json, JsonNull, JsonUniqtext-stored JSON
JsonBinary, JsonBinaryNull, JsonBinaryUniqbinary JSON (jsonb on Postgres)

UUID

VariantNotes
Uuid, UuidNull, UuidUniq
UuidWithDefault(String), UuidUniqWithDefault(String)default is a raw SQL expression string, e.g. "gen_random_uuid()" (via Expr::cust)

Bit strings

VariantNotes
VarBitLen(u32), VarBitLenNull(u32), VarBitLenUniq(u32)Postgres VARBIT(n)

Array

ItemSignatureAnchor
ColType::Array(ColumnType) / ArrayNull(ColumnType) / ArrayUniq(ColumnType)wrap a Sea-ORM ColumnType for the element typeschema.rs:276-278
ColType::array(kind: ArrayColType) -> Selfbuilds Array(..)schema.rs:298-300
ColType::array_uniq(kind: ArrayColType) -> Selfbuilds ArrayUniq(..)schema.rs:304-306
ColType::array_null(kind: ArrayColType) -> Selfbuilds ArrayNull(..)schema.rs:310-312
enum ArrayColType { String, Int, BigInt, Float, Double, Bool }element-type selector for the array* constructorsschema.rs:286-293

array_col_type maps each ArrayColType to a sea_orm::ColumnType: StringColumnType::string(None), IntInteger, BigIntBigInteger, FloatFloat, DoubleDouble, BoolBoolean (schema.rs:314-323).

Enum

VariantNotes
Enum(enum_name: String, variants: Vec<String>)NOT NULL
EnumNull(enum_name, variants)nullable
EnumWithDefault(enum_name, variants, default_value: String)NOT NULL + default
EnumNullWithDefault(enum_name, variants, default_value: String)nullable + default

(schema.rs:280-283)

Enum creation is backend-dependent and handled automatically by create_table/create_join_table (see Enum type semantics below) — you don’t call CREATE TYPE yourself.

Column-def helper functions (schema.rs’s own additions)

Beyond ColType, schema.rs defines a handful of standalone helpers used to build raw ColumnDef/TableCreateStatement/TableAlterStatement values, on top of everything re-exported from sea_orm_migration::schema:

FnSignatureBehaviorAnchor
alterfn alter<T: IntoIden + 'static>(name: T) -> TableAlterStatementTable::alter().table(name)schema.rs:19-21
table_auto_tzfn table_auto_tz<T>(name: T) -> TableCreateStatementTable::create().table(name).if_not_exists() with created_at/updated_at timestamptz columns already added (via timestamps_tz)schema.rs:24-29
timestamps_tzfn timestamps_tz(t: TableCreateStatement) -> TableCreateStatementadds created_at/updated_at as timestamp_with_time_zone columns defaulting to Expr::current_timestamp()schema.rs:34-39
timestamptzfn timestamptz<T>(name: T) -> ColumnDefnon-nullable timestamptz columnschema.rs:53-61
timestamptz_nullfn timestamptz_null<T>(name: T) -> ColumnDefnullable timestamptz columnschema.rs:42-50
enum_typefn enum_type<T>(name: T, enum_name: &str) -> ColumnDefnon-nullable enum columnschema.rs:64-72
enum_type_nullfn enum_type_null<T>(name: T, enum_name: &str) -> ColumnDefnullable enum columnschema.rs:75-83
enum_type_with_defaultfn enum_type_with_default<T>(name: T, enum_name: &str, default_value: &str) -> ColumnDefnon-nullable enum column + defaultschema.rs:93-102
enum_type_null_with_defaultfn enum_type_null_with_default<T>(name: T, enum_name: &str, default_value: &str) -> ColumnDefnullable enum column + defaultschema.rs:112-121

table_auto_tz is the timezone-aware counterpart to sea_orm_migration::schema::table_auto (which uses naive, non-tz timestamps) — create_table/create_join_table use table_auto_tz internally, so tables built through the DSL always get timezone-aware created_at/updated_at.

Table-level operations

All are async fn(m: &SchemaManager<'_>, ...) -> Result<(), DbErr>, called from a migration’s up/down.

FnSignatureAnchor
create_tablecreate_table(m, table: &str, cols: &[(&str, ColType)], refs: &[(&str, &str)])schema.rs:490-497
create_join_tablecreate_join_table(m, table, cols, refs) — composite primary key over the reference columnsschema.rs:512-519
create_table_without_timestampscreate_table_without_timestamps(m, table, cols, refs) — no auto created_at/updated_atschema.rs:537-544
create_join_table_without_timestampscreate_join_table_without_timestamps(m, table, cols, refs) — join table, no timestampsschema.rs:559-566
add_columnadd_column(m, table: &str, name: &str, atype: ColType)schema.rs:721-735
remove_columnremove_column(m, table: &str, name: &str)schema.rs:745-754
add_referenceadd_reference(m, fromtbl: &str, totbl: &str, refname: &str)schema.rs:764-839
remove_referenceremove_reference(m, fromtbl: &str, totbl: &str, refname: &str)schema.rs:849-892
drop_tabledrop_table(m, table: &str)schema.rs:902-906
add_enum_valuesadd_enum_values(m, enum_name: &str, new_values: Vec<String>)schema.rs:916-952
drop_enum_typedrop_enum_type(m, enum_name: &str)schema.rs:962-987

All four create_* functions share one implementation (create_table_impl, schema.rs:568-701), parameterized by is_join: bool and add_timestamps: bool.

// schema.rs:474-497 (doc example)
create_table(m, "movies", vec![
("title", ColType::String)
], vec![]).await;
Terminal window
loco g migration CreateMovies title:string user:references
loco g migration CreateMovies title:string user:references:admin_id

cols and refs parameters

  • cols: &[(&str, ColType)] — ordinary columns, in order, each turned into a ColumnDef via ColType::to_def.
  • refs: &[(&str, &str)] — one entry per foreign-key reference the new/altered table should carry. The first element names the referenced table (it is pluralized/snake-cased the same way as any table name); the second element is an optional custom FK column name — pass "" to use the default <singular(referenced_table)>_id (computed by reference_id, schema.rs:708-711).
    • Suffix the referenced-table name with ? to make the FK column nullable: refs: &[("user?", "")]create_table_impl strips the ? before normalizing the table name (schema.rs:664-669).
    • Nullable references get ON DELETE SET NULL / ON UPDATE NO ACTION; non-nullable references get ON DELETE CASCADE / ON UPDATE CASCADE (schema.rs:685-696).
    • The generated FK column is always ColType::BigInteger/BigIntegerNull (matching the i64 PkAuto default), unless a column of that name already exists in cols (schema.rs:676-683).
    • FK constraint name is deterministic: fk-{referenced_table}-{ref_column}-to-{table} (schema.rs:687) — note this is not the same naming order add_reference/remove_reference use (see next section): a FK added through create_table’s refs parameter is named fk-users-user_id-to-movies, whereas add_reference(m, "movies", "users", "") names it fk-movies-user_id-to-users. Calling remove_reference against a FK that was created via create_table’s refs (rather than via add_reference itself) will look for the wrong constraint name and not find it.

add_reference / remove_reference

Unlike the refs tuples above, add_reference/remove_reference take table names in natural “reads as” order — add_reference(m, "movies", "users", "") reads “movies belongs-to users”: fromtbl is the table being altered (movies), totbl is the table referenced (users).

// schema.rs:756-764 (doc example)
add_reference(m, "movies", "users", "").await;
// ...
remove_reference(m, "movies", "users", "").await;
  • add_reference always builds a ColType::BigInteger FK column, adds it via ALTER TABLE ... ADD COLUMN, and — on MySQL/Postgres only — also ADD FOREIGN KEY in the same statement. On SQLite it adds the column but skips the FK constraint (SQLite doesn’t allow adding FKs to an existing table; per Rails 5.2 convention, this is a documented no-op — schema.rs:817-830). Any other backend returns DbErr::BackendNotSupported { ctx: "add_reference" }.
  • remove_reference drops the named FK constraint on MySQL/Postgres; on SQLite it is a no-op for the same reason (schema.rs:879-883). Any other backend returns DbErr::BackendNotSupported { ctx: "remove_reference" }.

Enum type semantics per backend

create_table_impl scans cols for any ColType::Enum* variant and, for each distinct enum_name not yet seen, checks whether the type already exists (check_enum_exists, schema.rs:124-159, Postgres-only pg_type lookup) before creating it:

BackendBehavior
PostgresCreates a native CREATE TYPE ... AS ENUM (...) if it doesn’t already exist.
SQLiteNo native enum type; the column is created as TEXT with the enum behavior enforced via the column definition (no CREATE TYPE step).
MySQLNot created as a separate type; MySQL enums are inline in the column definition.
otherNo-op.

add_enum_values(m, enum_name, new_values) extends an existing enum: on Postgres it runs ALTER TYPE {enum_name} ADD VALUE '{value}' per new value; on SQLite/MySQL it’s a logged no-op (schema.rs:916-952). drop_enum_type(m, enum_name) runs DROP TYPE IF EXISTS {enum_name} CASCADE on Postgres (guarded by the same existence check) and is a no-op elsewhere (schema.rs:962-987).

Table naming

normalize_table(table: &str) -> String (schema.rs:704-706) pluralizes and snake-cases every table name passed to the DSL: cruet::to_plural(table).to_snake_case() — e.g. "person""people", "Movie""movies". This runs on every table-name argument across create_table, add_column, add_reference, etc., so callers pass singular or plural, either case, and get the same normalized table.

Timestamps: default vs _without_timestamps

create_table/create_join_table add created_at/updated_at (via table_auto_tz) unless you use the _without_timestamps variant (create_table_without_timestamps/create_join_table_without_timestamps), which builds a bare Table::create().if_not_exists() with no timestamp columns — full control over the schema.

The generator CLI flag that maps to the _without_timestamps functions is --without-tz (not --without-timestamps):

Terminal window
loco g migration CreatePosts title:string --without-tz
loco g migration CreateJoinTableUsersAndGroups count:int --without-tz
loco g scaffold posts title:string! user:references --api --without-tz

(src/cli.rs:193, :237, :241, :267)

An internal doc-comment inside schema.rs (schema.rs:533, on create_table_without_timestamps) still shows the old flag spelling --without-timestamps in its example — that comment is stale; the real CLI flag, wired in src/cli.rs, is --without-tz.

The loco g model|migration|scaffold field-type shorthand (e.g. title:string!, count:int^, user:references) maps onto this same ColType surface via loco-gen/src/mappings.json (col_type column). Notably, the generator’s int/unsigned shorthand also produces 64-bit columns (intColType::BigIntegerNull / Option<i64>, int!ColType::BigInteger / i64, unsigned family → BigUnsigned* / i64) — consistent with the PkAuto 64-bit default on this page. The full field-type table (all ~50 shorthand entries) belongs on the generators reference page, not yet published as of this writing.