"""Force every table created on MySQL to utf8mb4 + DYNAMIC row format. Importing this module installs a SQLAlchemy compiler hook. It is a side effect on purpose: the hook has to be registered before any CreateTable is compiled, and every place that creates tables needs it. WHY. Without it a CREATE TABLE inherits the SERVER's default charset. A MySQL box defaulting to latin1 - common on older installs, and the 5.6 server this was ported from is one - silently builds a latin1 schema that drifts from the utf8mb4 production target. Nothing fails at create time; it surfaces later as mangled characters, or as a join between a utf8mb4 and a latin1 column that cannot use an index. DYNAMIC row format is the other half: it keeps utf8mb4 indexes under the 767-byte prefix limit on pre-5.7 InnoDB, which is what error 1071 ("key too long") is. This lived inline in migrations/env.py, so it covered the CORE chain only. Plugin chains (ADR-008) run through shopdb/plugins/alembic_template.py, which never imported it - so a plugin's baseline tables were created at the server default while core's were utf8mb4, on the same database. Both import this now. Scoped to the mysql dialect, so the SQLite test database is untouched. """ from sqlalchemy.ext.compiler import compiles from sqlalchemy.schema import CreateTable TABLE_SUFFIX = ( " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" " COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC" ) @compiles(CreateTable, "mysql") def _mysql_create_table_utf8mb4(element, compiler, **kw): sql = compiler.visit_create_table(element, **kw) # An explicit CHARSET in the model's __table_args__ wins - this is a default, # not an override. if "CHARSET" not in sql.upper(): sql = sql.rstrip().rstrip(";") + TABLE_SUFFIX return sql