MySQL / MariaDB

One backend, rango-orm-mysql, covers MySQL 8.0+ and MariaDB 10.6+. It sticks to the subset both servers share, so the same models, queries and migration files work on either.

Setup

# Cargo.toml
[dependencies]
rango-orm = { version = "0.2", default-features = false, features = ["mysql"] }
# rango.toml
[database]
backend = "mysql"   # "mariadb" is accepted too
export DATABASE_URL=mysql://user:password@localhost:3306/mydb
# mariadb:// is accepted as an alias of mysql://

Connecting

#![allow(unused)]
fn main() {
use rango::prelude::*;

let pool = connect_mysql(&std::env::var("DATABASE_URL")?).await?;
}

Every connection uses utf8mb4, runs in UTC (DATETIME has no time zone) and in strict SQL mode, so invalid values are rejected instead of being silently truncated.

TLS: connections to a remote host use ssl-mode=REQUIRED unless the URL sets its own ssl-mode, so they never fall back to cleartext. Loopback and Unix socket connections are left as is. REQUIRED encrypts but does not check the server certificate: use ssl-mode=VERIFY_IDENTITY in production.

Type mapping

Rango typeMySQL / MariaDB column
FieldUuidBINARY(16)
FieldBoolBOOLEAN (TINYINT(1))
FieldInt / FieldBigInt / FieldSmallIntINT / BIGINT / SMALLINT
FieldAutoInt / FieldAutoBigIntINT / BIGINT AUTO_INCREMENT (id read back with LAST_INSERT_ID())
FieldDecimal<P, S>DECIMAL(P, S)
FieldVarchar<_, N>VARCHAR(N)
FieldTextLONGTEXT
FieldBytesLONGBLOB
FieldJsonJSON (a LONGTEXT with a json_valid check on MariaDB)
FieldDate / FieldTime / FieldDateTimeDATE / TIME(6) / DATETIME(6)

Tables are created with ENGINE=InnoDB DEFAULT CHARSET=utf8mb4.

Differences from Postgres

FeaturePostgresMySQL / MariaDB
RETURNING *✅❌: insert/update return the model you passed (every column is bound from it)
Transactional DDL✅❌: a migration that fails halfway leaves its earlier statements applied
Partial unique constraint (.condition(...))CREATE UNIQUE INDEX … WHEREskipped with a comment in the migration; enforce it in application code
get_or_createINSERT … ON CONFLICT DO NOTHING RETURNING *plain INSERT, then a SELECT on unique violation
Sub-microsecond timestamps✅truncated to microseconds

Migrations

rango makemigrations writes backtick-quoted SQL that also runs as-is in the mysql / mariadb client. Unique constraints, indexes and foreign keys are named ({table}_{col}_unique, {table}_{col}_idx, {table}_{col}_fkey) so later migrations can drop them. Type and nullability changes use MODIFY COLUMN.

MySQL caps identifiers at 64 characters. Constraint and index names that would exceed it are cut and suffixed with a stable hash. Table names are not shortened: if a table or M2M pivot name ({table}_{other_table}) is too long, makemigrations stops with an error. Use a shorter #[model(table = "...")] or model name.

rango migrate takes a GET_LOCK('rango_migrate') advisory lock, so two deploys cannot apply migrations at the same time.

Export / import

rango export writes BINARY(16) values as UUID strings and JSON columns as JSON (MariaDB included). rango import runs in one transaction with foreign key checks off for that session, so tables can appear in any order and a failed import leaves nothing behind. It converts UUID strings back to 16 bytes. Rows that already exist are skipped, or overwritten with --replace.

Local testing

just db-mysql     # MySQL 8.4 on :3307, MariaDB 11.4 on :3308 (Docker)
just test-mysql   # backend + CLI e2e tests against both