This project is designed for automated database schema migration from MySQL to PostgreSQL using SQLAlchemy and Alembic.
The toolkit provides functionality to:
- Introspect an existing MySQL database and generate SQLAlchemy models.
- Convert MySQL data types (TINYINT, LONGTEXT, UNSIGNED, etc.) to PostgreSQL equivalents.
- Fix identifier names (tables, indexes, FKs) that exceed the 63-character PostgreSQL limit.
- Patch Alembic migrations for PostgreSQL compatibility (boolean fixes, index de-duplication).
- Split a monolithic models file into individual class files.
The script performs a deep transformation of the models to ensure full compatibility with PostgreSQL:
| MySQL / Specific Code | PostgreSQL / SQLAlchemy | Comment |
|---|---|---|
LONGTEXT, MEDIUMTEXT |
Text |
Converted to universal text type |
VARCHAR(N), CHAR(N) |
String(N) |
charset and collation are removed |
DOUBLE, DECIMAL |
Float |
PostgreSQL prefers Float or Numeric for these |
TIMESTAMP, TIMESTAMP(fsp) |
DateTime |
Standard date and time type |
TINYINT(1) (flags) |
Boolean |
Applied if the field name matches patterns like is_, has_, etc. |
TINYINT (numbers) |
SmallInteger |
Applied if the field name doesn't match flag patterns |
unsigned=True |
(removed) | PostgreSQL does not have a native unsigned modifier |
charset='...', collation='...' |
(removed) | MySQL encoding parameters are incompatible with Postgres |
text('0') / text('1') |
text('false') / text('true') |
Only for fields defined as Boolean |
The script automatically determines whether a TINYINT field is logical (Boolean) or numeric (SmallInteger) by analyzing its name. Fields considered logical include:
- Prefixes:
is_,has_,can_,should_,was_. - Special names:
active,visible,public,enabled,deleted,revoked,archived, etc.
MySQL-specific construction:
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
is automatically replaced with the SQLAlchemy standard:
server_default=text('CURRENT_TIMESTAMP'), onupdate=func.now()
- Imports: The script removes
from sqlalchemy.dialects.mysql import ...and ensures all necessary types are present infrom sqlalchemy import .... - Syntax: Removes unnecessary empty parentheses in types (e.g.,
Integer()->Integer) for PEP8 compliance.
PostgreSQL has a strict 63-character limit on identifier lengths (tables, indexes, FKs). The script:
- Scans
models_pg.pyfor names exceeding this limit. - Applies the algorithm:
name_up_to_54_chars + _ + 8_char_md5_hash. - Hashing ensures that names remain unique across the entire database even after truncation.
This script is critical for "fixing" generated migrations that are originally tailored for MySQL:
- Bulk Type Replacement: Replaces
mysql.TINYINTin the migration file withsa.Boolean()orsa.SmallInteger(). - Index De-duplication: In PostgreSQL, index names must be unique within the schema. The script renames indexes like
school_idtoidx_table_name_school_id. - Empty Index Fix: Fixes erroneous
op.create_index(..., [])constructs by inserting the column name derived from the index name. - Server Default Translation: Changes
'0'/'1'to'false'/'true'for boolean columns at the DDL level.
Splits the monolithic file into a modular structure:
- Creates
models/base.pywith the declarative base class. - Generates one
.pyfile per table. - Creates
models/__init__.pyfor convenient importing of all models.
The easiest way is to run the orchestrator, which will guide you through all steps:
.\migrate.ps1chmod +x migrate.sh
./migrate.shThe orchestrator will perform the following steps:
- Prompt for MySQL connection string and generate
models.py. - Convert models to
models_pg.py. - Fix long identifier names.
- Patch the Alembic migration file.
- Split models into individual files in the
models/directory. - Assist in creating
.envand starting PostgreSQL in Docker. - Apply migrations (
alembic upgrade head). - Prompt for cleanup of intermediate files.
If you prefer to run steps individually, use the scripts in the scripts/ directory in order:
uv run --with sqlacodegen --with pymysql sqlacodegen mysql+pymysql://user:pass@host:port/db_name > models.pypython scripts/01_convert_models.py models.py -o models_pg.pypython scripts/02_patch_identifiers.pypython scripts/03_patch_migration.pypython scripts/04_split_models.py models_pg.py -o models- Configure
.env(seeenv.example). - Start DB:
docker-compose up -d. - Apply schema:
uv run alembic upgrade head.
main.py— Main migration orchestrator.scripts/— Toolkit for code transformation.alembic/— Configuration and migration history.models/— Final models directory (generated automatically).docker-compose.yml— PostgreSQL 16 environment..gitignore— Properly configured to exclude generated and sensitive data.
This project is licensed under the MIT License - see the LICENSE file for details.