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-332Self::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()).
| Variant | Builds | Rust type | Anchor |
|---|---|---|---|
ColType::PkAuto | big_pk_auto(name) — auto-increment BIGINT PK | i64 | schema.rs:332 |
ColType::PkUuid | pk_uuid(name) — UUID PK, no auto-increment | Uuid | schema.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.*Uniq—NOT NULL+ unique index.*WithDefault(v)—NOT NULL+ a default value.*Len(n)— fixed/max length, forChar/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
| Variant | Notes |
|---|---|
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
| Variant | Rust type | Notes |
|---|---|---|
Integer, IntegerNull, IntegerUniq, IntegerWithDefault(i32) | i32 | 32-bit signed |
SmallInteger, SmallIntegerNull, SmallIntegerUniq, SmallIntegerWithDefault(i16) | i16 | |
BigInteger, BigIntegerNull, BigIntegerUniq, BigIntegerWithDefault(i64) | i64 | also 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
| Variant | Notes |
|---|---|
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
| Variant | Notes |
|---|---|
Boolean, BooleanNull, BooleanWithDefault(bool) | no *Uniq variant |
Date / time
| Variant | Notes |
|---|---|
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
| Variant | Notes |
|---|---|
Binary, BinaryNull, BinaryUniq | unbounded, 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
| Variant | Notes |
|---|---|
Json, JsonNull, JsonUniq | text-stored JSON |
JsonBinary, JsonBinaryNull, JsonBinaryUniq | binary JSON (jsonb on Postgres) |
UUID
| Variant | Notes |
|---|---|
Uuid, UuidNull, UuidUniq | |
UuidWithDefault(String), UuidUniqWithDefault(String) | default is a raw SQL expression string, e.g. "gen_random_uuid()" (via Expr::cust) |
Bit strings
| Variant | Notes |
|---|---|
VarBitLen(u32), VarBitLenNull(u32), VarBitLenUniq(u32) | Postgres VARBIT(n) |
Array
| Item | Signature | Anchor |
|---|---|---|
ColType::Array(ColumnType) / ArrayNull(ColumnType) / ArrayUniq(ColumnType) | wrap a Sea-ORM ColumnType for the element type | schema.rs:276-278 |
ColType::array(kind: ArrayColType) -> Self | builds Array(..) | schema.rs:298-300 |
ColType::array_uniq(kind: ArrayColType) -> Self | builds ArrayUniq(..) | schema.rs:304-306 |
ColType::array_null(kind: ArrayColType) -> Self | builds ArrayNull(..) | schema.rs:310-312 |
enum ArrayColType { String, Int, BigInt, Float, Double, Bool } | element-type selector for the array* constructors | schema.rs:286-293 |
array_col_type maps each ArrayColType to a sea_orm::ColumnType: String → ColumnType::string(None), Int → Integer, BigInt → BigInteger, Float → Float, Double → Double, Bool → Boolean (schema.rs:314-323).
Enum
| Variant | Notes |
|---|---|
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:
| Fn | Signature | Behavior | Anchor |
|---|---|---|---|
alter | fn alter<T: IntoIden + 'static>(name: T) -> TableAlterStatement | Table::alter().table(name) | schema.rs:19-21 |
table_auto_tz | fn table_auto_tz<T>(name: T) -> TableCreateStatement | Table::create().table(name).if_not_exists() with created_at/updated_at timestamptz columns already added (via timestamps_tz) | schema.rs:24-29 |
timestamps_tz | fn timestamps_tz(t: TableCreateStatement) -> TableCreateStatement | adds created_at/updated_at as timestamp_with_time_zone columns defaulting to Expr::current_timestamp() | schema.rs:34-39 |
timestamptz | fn timestamptz<T>(name: T) -> ColumnDef | non-nullable timestamptz column | schema.rs:53-61 |
timestamptz_null | fn timestamptz_null<T>(name: T) -> ColumnDef | nullable timestamptz column | schema.rs:42-50 |
enum_type | fn enum_type<T>(name: T, enum_name: &str) -> ColumnDef | non-nullable enum column | schema.rs:64-72 |
enum_type_null | fn enum_type_null<T>(name: T, enum_name: &str) -> ColumnDef | nullable enum column | schema.rs:75-83 |
enum_type_with_default | fn enum_type_with_default<T>(name: T, enum_name: &str, default_value: &str) -> ColumnDef | non-nullable enum column + default | schema.rs:93-102 |
enum_type_null_with_default | fn enum_type_null_with_default<T>(name: T, enum_name: &str, default_value: &str) -> ColumnDef | nullable enum column + default | schema.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.
| Fn | Signature | Anchor |
|---|---|---|
create_table | create_table(m, table: &str, cols: &[(&str, ColType)], refs: &[(&str, &str)]) | schema.rs:490-497 |
create_join_table | create_join_table(m, table, cols, refs) — composite primary key over the reference columns | schema.rs:512-519 |
create_table_without_timestamps | create_table_without_timestamps(m, table, cols, refs) — no auto created_at/updated_at | schema.rs:537-544 |
create_join_table_without_timestamps | create_join_table_without_timestamps(m, table, cols, refs) — join table, no timestamps | schema.rs:559-566 |
add_column | add_column(m, table: &str, name: &str, atype: ColType) | schema.rs:721-735 |
remove_column | remove_column(m, table: &str, name: &str) | schema.rs:745-754 |
add_reference | add_reference(m, fromtbl: &str, totbl: &str, refname: &str) | schema.rs:764-839 |
remove_reference | remove_reference(m, fromtbl: &str, totbl: &str, refname: &str) | schema.rs:849-892 |
drop_table | drop_table(m, table: &str) | schema.rs:902-906 |
add_enum_values | add_enum_values(m, enum_name: &str, new_values: Vec<String>) | schema.rs:916-952 |
drop_enum_type | drop_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;loco g migration CreateMovies title:string user:referencesloco g migration CreateMovies title:string user:references:admin_idcols and refs parameters
cols: &[(&str, ColType)]— ordinary columns, in order, each turned into aColumnDefviaColType::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 byreference_id,schema.rs:708-711).- Suffix the referenced-table name with
?to make the FK column nullable:refs: &[("user?", "")]—create_table_implstrips the?before normalizing the table name (schema.rs:664-669). - Nullable references get
ON DELETE SET NULL/ON UPDATE NO ACTION; non-nullable references getON DELETE CASCADE/ON UPDATE CASCADE(schema.rs:685-696). - The generated FK column is always
ColType::BigInteger/BigIntegerNull(matching the i64PkAutodefault), unless a column of that name already exists incols(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 orderadd_reference/remove_referenceuse (see next section): a FK added throughcreate_table’srefsparameter is namedfk-users-user_id-to-movies, whereasadd_reference(m, "movies", "users", "")names itfk-movies-user_id-to-users. Callingremove_referenceagainst a FK that was created viacreate_table’srefs(rather than viaadd_referenceitself) will look for the wrong constraint name and not find it.
- Suffix the referenced-table name with
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_referencealways builds aColType::BigIntegerFK column, adds it viaALTER TABLE ... ADD COLUMN, and — on MySQL/Postgres only — alsoADD FOREIGN KEYin 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 returnsDbErr::BackendNotSupported { ctx: "add_reference" }.remove_referencedrops 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 returnsDbErr::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:
| Backend | Behavior |
|---|---|
| Postgres | Creates a native CREATE TYPE ... AS ENUM (...) if it doesn’t already exist. |
| SQLite | No native enum type; the column is created as TEXT with the enum behavior enforced via the column definition (no CREATE TYPE step). |
| MySQL | Not created as a separate type; MySQL enums are inline in the column definition. |
| other | No-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):
loco g migration CreatePosts title:string --without-tzloco g migration CreateJoinTableUsersAndGroups count:int --without-tzloco 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, oncreate_table_without_timestamps) still shows the old flag spelling--without-timestampsin its example — that comment is stale; the real CLI flag, wired insrc/cli.rs, is--without-tz.
Related: generator field-type mapping
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 (int → ColType::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.