MySQL & MariaDB¶
DBWarden treats MySQL (and its fork MariaDB) as first-class backends: every natively supported feature is reverse-engineered, diffed, and emitted as correct DDL.
First-Class Features¶
"First-class" means the round-trip is verified: reverse-engineer a live database with generate-models, feed the output back into make-migrations, and get zero diff.
# Step 1: reverse-engineer your live MySQL/MariaDB database
$ dbwarden generate-models -d primary
# Step 2: feed the generated models back in, zero diff
$ dbwarden make-migrations -d primary
# -> "No new migrations to generate" (output is empty; your models match the DB exactly)
The following MySQL/MariaDB features are fully supported in this round-trip:
| Category | Features |
|---|---|
| Engine | ENGINE=InnoDB, MyISAM, etc. via my_engine |
| Charset & Collation | Table: DEFAULT CHARACTER SET / COLLATE via my_charset, my_collate. Column: per-column CHARACTER SET / COLLATE via my.field(charset=..., collate=...) |
| Row Format | ROW_FORMAT=DYNAMIC, COMPACT, COMPRESSED, REDUNDANT via my_row_format |
| Auto Increment | Table-level AUTO_INCREMENT=N via my_auto_increment. Column-level toggle via autoincrement field |
| Unsigned | UNSIGNED on integer columns via my.field(unsigned=True) |
| ON UPDATE | ON UPDATE CURRENT_TIMESTAMP via my.field(on_update="CURRENT_TIMESTAMP") |
| Comments | Table: ALTER TABLE t COMMENT = '...'. Column: MODIFY COLUMN ... COMMENT '...' (full column definition preserved) |
| Foreign Keys | ON DELETE / ON UPDATE options; DROP uses DROP FOREIGN KEY (MySQL syntax) |
| Indexes | Full index support; USING BTREE / HASH preserved |
| Auto-increment Lifecycle | Toggle autoincrement on integer PKs via autoincrement field: generates MODIFY COLUMN ... AUTO_INCREMENT |
| Type Normalization | TINYINT(1) -> BOOLEAN, INT, BIGINT, VARCHAR(n), TEXT, DATETIME, TIMESTAMP, YEAR, DECIMAL(p,s), FLOAT, DOUBLE, BLOB, JSON, ENUM, SET |
MariaDB-Specific Features¶
| Category | Features |
|---|---|
| Page Compression | PAGE_COMPRESSED=1 / PAGE_COMPRESSION_LEVEL=N via mdb_page_compressed, mdb_page_compression_level on MdbTableMeta |
| Invisible Columns | Column invisibility via mdb.field(invisible=True) on MdbColumnMeta |
| Sequences | CREATE SEQUENCE support via mdb.field(sequence=...) |
Installation¶
Install with the MySQL driver:
Or with uv:
Configuration¶
The MySQL backend is enabled by setting database_type="mysql" (or database_type="mariadb") in your dbwarden config:
from dbwarden import database_config
database_config(
database_name="primary",
default=True,
database_type="mysql",
database_url_sync="mysql+pymysql://user:password@localhost:3306/mydb",
)
The connection URL uses the mysql+pymysql:// scheme (the pymysql driver is included via the [mysql] extra). You can also use any SQLAlchemy-compatible MySQL driver such as mysql+mysqlconnector://.
Declaring Metadata¶
MySQL/MariaDB metadata is declared in a class Meta inner class on the model. This is the only supported surface: mapped_column(info=...) raises DBWardenConfigError.
Table-Level Meta¶
Inherit from MyTableMeta on your class Meta:
from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from dbwarden.databases.mysql import MyTableMeta
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String(255))
class Meta(MyTableMeta):
my_engine = "InnoDB"
my_charset = "utf8mb4"
my_collate = "utf8mb4_unicode_ci"
my_row_format = "DYNAMIC"
my_auto_increment = 1000
comment = "Core user accounts"
MyTableMeta inherits from TableMeta, which provides common attributes shared across all backends:
| Attribute | Type | SQL |
|---|---|---|
comment |
str |
ALTER TABLE t COMMENT = '...' |
indexes |
list[dict] |
CREATE INDEX ... |
checks |
list[dict] |
ALTER TABLE t ADD CONSTRAINT ... CHECK (...) |
uniques |
list[dict] |
ALTER TABLE t ADD CONSTRAINT ... UNIQUE (...) |
MySQL-specific MyTableMeta attributes:
| Attribute | Type | SQL |
|---|---|---|
my_engine |
str |
ALTER TABLE t ENGINE = name |
my_charset |
str |
ALTER TABLE t DEFAULT CHARACTER SET name |
my_collate |
str |
ALTER TABLE t COLLATE = name |
my_row_format |
str |
ALTER TABLE t ROW_FORMAT = name |
my_auto_increment |
int |
ALTER TABLE t AUTO_INCREMENT = N |
For MariaDB, use MdbTableMeta:
from dbwarden.databases.mariadb import MdbTableMeta
class Meta(MdbTableMeta):
my_engine = "InnoDB"
mdb_page_compressed = True
mdb_page_compression_level = 3
MariaDB-specific MdbTableMeta attributes (in addition to all MyTableMeta attributes):
| Attribute | Type | SQL |
|---|---|---|
mdb_page_compressed |
bool |
PAGE_COMPRESSED=1 |
mdb_page_compression_level |
int |
PAGE_COMPRESSION_LEVEL=N |
Column-Level Meta¶
Use MyColumnMeta inner classes for per-column metadata. The inner class must be named after the column. Use my = my.field(...) to set column-level options:
from sqlalchemy import Integer, String, Text, TIMESTAMP
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from dbwarden.databases.mysql import MyTableMeta, MyColumnMeta, my
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String(255))
bio: Mapped[str] = mapped_column(Text)
updated_at: Mapped[str] = mapped_column(TIMESTAMP)
class Meta(MyTableMeta):
class id(MyColumnMeta):
comment = "Primary key"
my = my.field(unsigned=True)
class email(MyColumnMeta):
my = my.field(charset="utf8mb4", collate="utf8mb4_unicode_ci")
class updated_at(MyColumnMeta):
my = my.field(on_update="CURRENT_TIMESTAMP")
MyColumnMeta includes common column attributes shared across all backends:
| Attribute | Type | SQL |
|---|---|---|
comment |
str |
MODIFY COLUMN ... COMMENT '...' |
public |
bool |
Controls field visibility in schemap auto-schema |
my |
MyFieldSpec |
MySQL-specific column options (see table below) |
MySQL-specific MyFieldSpec fields (set via my.field(...)):
| Keyword | Type | SQL |
|---|---|---|
unsigned |
bool |
UNSIGNED on integer columns |
charset |
str |
CHARACTER SET name (per-column charset) |
collate |
str |
COLLATE name (per-column collation) |
on_update |
str |
ON UPDATE CURRENT_TIMESTAMP (typically on TIMESTAMP columns) |
For MariaDB, use MdbColumnMeta and mdb.field(...):
from dbwarden.databases.mariadb import MdbColumnMeta
from dbwarden.databases.mariadb import mdb
class Meta(MdbTableMeta):
class id(MdbColumnMeta):
mdb = mdb.field(invisible=True)
MariaDB-specific MdbFieldSpec fields (set via mdb.field(...)):
| Keyword | Type | SQL |
|---|---|---|
invisible |
bool |
ALTER TABLE ... ALTER COLUMN c SET INVISIBLE |
sequence |
str |
Sequence name for MariaDB sequence support |
unsigned |
bool |
UNSIGNED on integer columns |
charset |
str |
CHARACTER SET name |
collate |
str |
COLLATE name |
on_update |
str |
ON UPDATE CURRENT_TIMESTAMP |
Foreign Key Options¶
Foreign key options (ondelete, onupdate) are captured from the database by generate-models and emitted in the ForeignKey constructor:
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
class OrderItem(Base):
__tablename__ = "order_items"
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id", ondelete="CASCADE"), nullable=False)
Model Example (Generated)¶
Here is the complete generated model output for a MySQL table with engine, charset, unsigned PK, ON UPDATE, and per-column charset:
from sqlalchemy import BigInteger, Column, Integer, String, TIMESTAMP, Text, text
from sqlalchemy.orm import DeclarativeBase
Base = declarative_base()
from dbwarden.databases.mysql import MyColumnMeta, MyTableMeta, my
class User(Base):
__tablename__ = 'users'
id = Column('id', Integer, primary_key=True, nullable=False)
email = Column('email', String(255), nullable=False)
bio = Column('bio', Text)
updated_at = Column('updated_at', TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
class Meta(MyTableMeta):
my_engine = 'InnoDB'
my_charset = 'utf8mb4'
my_collate = 'utf8mb4_unicode_ci'
my_row_format = 'DYNAMIC'
comment = 'Core user accounts'
class id(MyColumnMeta):
comment = 'Primary key'
my = my.field(unsigned=True)
class email(MyColumnMeta):
my = my.field(charset='utf8mb4', collate='utf8mb4_unicode_ci')
class updated_at(MyColumnMeta):
my = my.field(on_update='CURRENT_TIMESTAMP')
DDL Behavior¶
DDL Is NOT Transactional¶
MySQL and MariaDB DDL is non-transactional: each DDL statement implicitly commits the current transaction. If a migration file contains multiple statements and one fails, the prior DDL cannot be rolled back. This makes MySQL/MariaDB more fragile than PostgreSQL for automated migration runs.
Column Type Changes¶
Emits ALTER TABLE t MODIFY COLUMN c newtype. Unlike PostgreSQL, MySQL requires the full column definition on every MODIFY COLUMN. DBWarden handles this by re-emitting all column attributes (type, unsigned, nullable, default, comment, charset, collate, auto_increment) in a single statement:
Column Nullable Changes¶
Emits ALTER TABLE t MODIFY COLUMN c type [NULL | NOT NULL], again with the full column type.
Column Meta Changes¶
When MySQL-specific column metadata changes (unsigned, charset, collate, on_update), DBWarden generates a full MODIFY COLUMN that preserves the column's type, nullable, default, comment, and autoincrement state:
Table Option Changes¶
MySQL table-level option changes generate individual ALTER TABLE statements:
| Change | Generated SQL |
|---|---|
| Engine | ALTER TABLE t ENGINE = InnoDB |
| Charset | ALTER TABLE t DEFAULT CHARACTER SET utf8mb4 |
| Collation | ALTER TABLE t COLLATE = utf8mb4_unicode_ci |
| Row Format | ALTER TABLE t ROW_FORMAT = DYNAMIC |
| Auto Increment | ALTER TABLE t AUTO_INCREMENT = 1000 |
Auto-increment Lifecycle¶
DBWarden supports toggling auto-increment on integer primary key columns. The autoincrement field in your model controls whether a column uses auto-increment:
class User(Base):
__tablename__ = "users"
# Autoincrement enabled: same as default behavior
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
class Meta(MyTableMeta):
class id(MyColumnMeta):
pass # uses default autoincrement from model
To explicitly disable auto-increment on a PK column:
class User(Base):
__tablename__ = "users"
# Plain integer PK: no auto-increment
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
What happens when autoincrement changes:
| Change | Generated SQL |
|---|---|
| Add autoincrement | ALTER TABLE t MODIFY COLUMN c INT NOT NULL AUTO_INCREMENT |
| Remove autoincrement | ALTER TABLE t MODIFY COLUMN c INT NOT NULL |
Comments¶
Unlike PostgreSQL, MySQL has no COMMENT ON syntax. DBWarden generates the correct MySQL syntax:
-- Table comment
ALTER TABLE users COMMENT = 'Core user accounts';
-- Column comment (full MODIFY COLUMN preserving all attributes)
ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NOT NULL COMMENT 'User email address';
When a comment is cleared, MySQL syntax is used:
ALTER TABLE users COMMENT = '';
ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NOT NULL COMMENT '';
Snapshot Format¶
When database_type is "mysql" or "mariadb", the snapshot captures MySQL-specific metadata in my_column and my_table blocks.
Column Extras¶
{
"columns": {
"id": {
"name": "id",
"type": "int",
"nullable": false,
"default": null,
"autoincrement": true,
"primary_key": true,
"comment": "Primary key",
"my_column": {
"my_unsigned": true,
"my_charset": null,
"my_collate": null,
"my_on_update": null
}
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"nullable": false,
"default": "CURRENT_TIMESTAMP",
"autoincrement": false,
"primary_key": false,
"my_column": {
"my_unsigned": false,
"my_on_update": "CURRENT_TIMESTAMP"
}
}
}
}
Table Extras¶
{
"my_table": {
"my_engine": "InnoDB",
"my_charset": "utf8mb4",
"my_collate": "utf8mb4_unicode_ci",
"my_row_format": "Dynamic",
"my_auto_increment": 1000
}
}
For MariaDB, additional fields appear:
Reverse Engineering¶
generate-models queries information_schema.TABLES and information_schema.COLUMNS to reverse-engineer all MySQL/MariaDB metadata. The emitted model uses class Meta with MyTableMeta and MyColumnMeta inner classes.
Generated output includes automatic detection of:
- Engine, charset, collation, row format from information_schema.TABLES
- Column unsigned, charset, collation, on_update from information_schema.COLUMNS
- Foreign key options (ON DELETE, ON UPDATE)
- Auto-increment columns
- Column comments
Safety Classification¶
DBWarden classifies migration changes using the Safety enum:
from dbwarden.engine.safety import Safety
assert Safety.SAFE == "SAFE"
assert Safety.INFO == "INFO"
assert Safety.WARN == "WARN"
assert Safety.CRITICAL == "CRITICAL"
| Change Type | Severity | Flag Required |
|---|---|---|
| Add column | INFO |
None |
| Drop column | WARNING |
--force |
| Change column type | WARNING |
--force |
| Change column nullable | WARNING |
--force |
| Change column comment | INFO |
None |
| Change MySQL column meta | WARNING |
--force |
| Change engine | INFO |
None |
| Change charset | INFO |
None |
| Change collation | INFO |
None |
| Change row format | INFO |
None |
| Change auto_increment | INFO |
None |
| Change table comment | INFO |
None |
| Add / drop index | INFO / WARNING |
--force |
| Add / drop FK | INFO / WARNING |
--force |