Skip to content
★ GitHubGet 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).

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()).

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

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.

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
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
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
Variant Notes
Boolean, BooleanNull, BooleanWithDefault(bool) no *Uniq variant
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
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
Variant Notes
Json, JsonNull, JsonUniq text-stored JSON
JsonBinary, JsonBinaryNull, JsonBinaryUniq binary JSON (jsonb on Postgres)
Variant Notes
Uuid, UuidNull, UuidUniq
UuidWithDefault(String), UuidUniqWithDefault(String) default is a raw SQL expression string, e.g. "gen_random_uuid()" (via Expr::cust)
Variant Notes
VarBitLen(u32), VarBitLenNull(u32), VarBitLenUniq(u32) Postgres VARBIT(n)
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: StringColumnType::string(None), IntInteger, BigIntBigInteger, FloatFloat, DoubleDouble, BoolBoolean (schema.rs:314-323).

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)

Section titled “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.

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
rename_column rename_column(m, table: &str, from: &str, to: &str) — renames a column, keeping its type and data. This is what the Rename<Old>To<New>On<Table> migration name generates (migration/rename_column.t) schema.rs:768-781
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;
Terminal window
loco g migration CreateMovies title:string user:references
loco g migration CreateMovies title:string user:references:admin_id
  • 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.

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" }.

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).

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

Section titled “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 --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/column.rs (the parse_column function and ScalarType enum). 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.