MySQL and MariaDB
MySQL and MariaDB share one planner and renderer family, but they are separate
dialects with different capability sets. Pass an explicit --dialect mysql or
--dialect mariadb in examples and CI jobs, and treat a plan reviewed for one
variant as unreviewed for the other. Differences that show up in generated
SQL:
- Enums are inline
ENUMcolumn types, not standalone type objects. - MariaDB guards constraint and index drops with
IF EXISTS; MySQL rejects that guard, so themysqlrenderer strips it. - The
DROP CHECKspelling exists only on MySQL 8.0.16+; MariaDB uses the genericDROP CONSTRAINTclause. - Portable foreign keys require InnoDB tables and compatible column types,
signedness, character sets, and collations. MariaDB generated FK columns and
MySQL virtual generated FK columns fail before rendering. MySQL stored
generated columns reject referential actions the engine cannot apply. When
an FK-participating table has no declared engine, Ptah emits
ENGINE=InnoDBexplicitly instead of trusting the session default. SET NULLrequires nullable local columns. Explicit foreign-key names are limited to 64 characters; generated names are shortened deterministically.- A nonunique referenced key must be a complete leftmost BTREE prefix. FULLTEXT, SPATIAL, HASH, parser-backed, expression, and prefix indexes do not qualify.
- A modified
SQL SECURITY DEFINERroutine is refused before migration SQL is planned when its catalogDEFINERdiffers from the connectedCURRENT_USER(). Connect as that definer, change the desired routine toSQL SECURITY INVOKER, or leave the foreign routine unchanged. Missing ownership facts fail closed too. - An index declaring
SPATIALorFULLTEXTis compared against the access method the server reports, so a plain index of the same name over the same column is a difference rather than a match. An index declaring no type accepts whatever the engine chose, and the asymmetry is the engines’:CREATE INDEXover aPOINTcolumn leavesINDEX_TYPE=BTREEon MariaDB 11.8 andSPATIALon MySQL 8.4, so comparing an undeclared type would plan a rebuild on MySQL that MySQL immediately undoes. USING BTREEandUSING HASHare read on aKEY,INDEXorUNIQUE KEYand rendered back after the column list. AUNIQUE KEYasking for a method is read as a unique index, which is what the server builds and where a method has somewhere to live.BTREEis not carried:INDEX_TYPEreports it for a declaredUSING BTREEand for an index that asked for nothing alike, so the two are one index to every reader Ptah has, and emitting it would put the clause into the DDL of every index read back from a server.HASHis carried, and whether the server honors it belongs to the storage engine rather than to the dialect: on InnoDB, MySQL 8.4 recordsBTREEand drops the clause fromSHOW CREATE TABLEwhile MariaDB 11.8 recordsHASHand prints it back, and onMEMORYboth recordHASH. MariaDB records it on every engine that takes an index –InnoDB,MEMORY,MyISAMandAriaall reportHASH, andARCHIVErefuses an index at all – so there a desiredHASHagainst a server reporting otherwise is a real difference and is reported. MySQL records it only onMEMORY, so the comparison stays quiet there for the same reason the undeclared type above is not compared: on the default engine a desiredHASHreads back asBTREE, and reporting it would plan a rebuild MySQL immediately undoes. Deciding the MySQL case properly needs the table’s storage engine, which the index comparison does not have.- Two constraints on one table may share a name, and both engines accept
CONSTRAINT same UNIQUE (a)besideCONSTRAINT same FOREIGN KEY (a). Ptah identifies a named constraint by its type as well as its table and name, so both survive the read, the desired model, and the comparison. MySQL also lets aCHECKshare a name with aUNIQUE, where MariaDB answersERROR 1826. - A key part’s direction is read back from the catalog, so
KEY (a DESC)andKEY (a)are told apart rather than both arriving ascending. It also decides which index a foreign key owns: MySQL will not back one with a descending leading part and builds its own index instead, while MariaDB reuses whatever covers the columns. So a same-named index beside a covering one is the author’s on MariaDB and the engine’s on MySQL, and only the first is planned for removal. - A key part may be an expression on MySQL –
KEY ((a + 1)), a functional key part – and an unnamed one takes the name the server gives it,functional_index, thenfunctional_index_2andfunctional_index_3. Two refusals go with it, and they are different facts rather than one rule: MariaDB has no functional key parts at all and answersERROR 1064to every spelling, so the dialect decides; MySQL accepts them in an index and refuses one in aPRIMARY KEYwithERROR 3756, so that refusal holds on both engines. A functional part in a table-bodyUNIQUE KEYis read as a unique index rather than as a constraint, which is what the server builds: MySQL reports one index withNON_UNIQUE=0, a null column and the expression, and a constraint has nowhere to keep an expression. - An inline
KEY,INDEXorUNIQUE KEYthe author did not name is read with the name its server would assign: the first key part’s column, then_2,_3for a name already taken. A prefix length and aDESCdirection stay out of the name, and a column-levelUNIQUEclaims its column before any index does. The name is decided when the SQL is read rather than when it is written, because the catalog reports what the server chose and a desired schema that guessed differently would never converge with it. - A non-ASCII index name is refused, rather than compared. The two engines fold
such names differently and not in a way one rule covers: measured on MySQL
8.4.11 and MariaDB 11.8.9 over a
utf8mb4connection,Ibeside dotlessıandΣbeside finalςare accepted by MySQL and answerERROR 1061on MariaDB, while dottedİbesideiand the Kelvin sign besideKdo the opposite. A loneprımaryis accepted by MySQL and answersERROR 1280on MariaDB, which is why a solitary non-ASCII name is not a safe exception either – it is still an unresolved comparison against the reservedPRIMARY. ASCII folding is shared and deterministic and is unchanged; a name derived from a non-ASCII column is refused for the same reason an explicit one is. - Migration planning refuses a plan whose index names may collide, rather than
emitting one the server rejects halfway. A name carrying a non-ASCII rune has
an equivalence class Ptah cannot compute offline, so it is treated as a
possible conflict with every other index name on that table, ASCII ones
included –
İcollides with plainion MySQL, so grouping only the non-ASCII names together would still miss it. Measured, the alternative was a half-applied migration: renderingCREATE INDEX İandCREATE INDEX iagainst MySQL 8.4.11 creates the first and answersERROR 1061on the second. - A comparison that reaches the target asks it instead of guessing. The
conservative rule above is what an offline run still does, and it was the
only answer until the names could be resolved against the server:
abesideäis accepted by both engines and was reported as a possible conflict for want of anything better. A comparison holding a connection now asks, and gets the engine’s own answer for each pair. What is asked is the collision rather than a fold to imitate, because neither engine exposes the fold its identifier comparison uses – measured, none ofLOWER(),utf8mb4_general_ci,utf8mb4_unicode_ci,utf8mb4_uca1400_ai_ciorutf8mb4_binreproduces MariaDB’s. A temporary table carrying the names as keys either creates or answersERROR 1061, which is exactly the question, and a temporary table is this session’s alone and gives the per-table namespace an index name actually lives in. Only names Ptah cannot fold are asked about, so a schema whose index names are all ASCII reaches no server and pays nothing. Sixty-four keys is the per-table ceiling on both engines, so a larger set is asked in several statements, each carrying every non-ASCII name so no pairing is missed. A refusal that is not the duplicate-name answer – a lost connection, or an account withoutCREATE TEMPORARY TABLES, which answersERROR 1044– is reported rather than read as an equivalence. - Column names are compared ASCII-case-insensitively, and a non-ASCII one is
treated as a possible conflict with every column in its table. Both engines
fold ASCII case: a table declaring
AandaanswersERROR 1060, and a foreign key writtenabinds to a column declaredAand reuses its key – so modeling these names as exact reported drift on every run and missed a duplicate the server refuses. Beyond ASCII the two disagree, and the same fold decides both questions: MySQL callsİ/iand the Kelvin sign/Kone column and MariaDB callsI/ıandσ/ςone column, each engine accepting a foreign key written with either spelling of a pair it folds and reporting a missing key column for a pair it does not. The ASCII half of a pair is why the conflict is table-wide rather than per name:İcollides with plainion MySQL. - A non-ASCII column named by a key, a constraint, or its own
UNIQUEis refused for the same reason, and the disagreement runs deeper there. Asked whether two columns differing only by the pair are one name, MySQL folds dottedİ/iand the Kelvin sign/Kwhile MariaDB foldsI/ıandσ/ς– and MariaDB folds the Kelvin pair for that question while treating the two as different columns when it resolves a foreign key, so the rule is not one per engine either. A column nothing keys takes part in no comparison and is kept. - Two indexes on one table claiming one name are refused. Both engines answer
ERROR 1061 Duplicate key name, so accepting it would describe a table neither can create.KEY (a), KEY a (b)is that shape: the unnamed index takesaas soon as it is read, and the later explicitacollides with it. - A table body declares a spatial or full-text index as
{SPATIAL|FULLTEXT} [INDEX|KEY] [name] (columns), and every optional part of that is optional here too —FULLTEXT (bio)is as readable asFULLTEXT INDEX ft_bio (bio). TheWITH PARSER <name>clause travels with it.KEYmatters as much asINDEX: both dump tools normalize to it, so a table written withFULLTEXT INDEXcomes back out ofmysqldumpasFULLTEXT KEY. An index left unnamed takes the name its server would give it, by the rule above. - A column carrying both a primary key and a
UNIQUEis written back the way it was read, because the two spellings do not mean the same thing.a INT UNIQUE, PRIMARY KEY (a)builds the primary key and a secondary unique index namedaon both engines, and it is rendered as the table-level key it was;a INT PRIMARY KEY UNIQUEbuilds both on MySQL and the primary key alone on MariaDB, and it is rendered inline so each engine gives its own answer. Folding the first into the second would lose MariaDB’s second index. - DDL commits implicitly on both engines, so a failed migration cannot be rolled back by the surrounding transaction.
Dev-database cleanup privileges
Section titled “Dev-database cleanup privileges”Database-realm cleanup requires global SELECT, DROP, ALTER,
ALTER ROUTINE, EVENT, LOCK TABLES, and PROCESS. MySQL also requires
global TRIGGER and, on MySQL 8.0.20 and newer, SHOW_ROUTINE; MariaDB
requires global SHOW VIEW. Ptah verifies this privilege set before destructive
DDL. Cleanup fails closed when another user database contains a routine, event,
or trigger because its body can reference the cleanup realm without a catalog
dependency. Grant these privileges only to credentials used with a dedicated
disposable dev database.
Online DDL for large tables
Section titled “Online DDL for large tables”For large tables, ptah migrations up and down can route ALTER TABLE
statements through gh-ost or pt-online-schema-change, either per migration
with a -- +ptah online_ddl_tool=ghost directive or automatically above a
configured row-count threshold:
online_ddl: tool: ghost threshold_rows: 1000000A tool-routed migration runs on the tool’s own connections and is not atomic:
keep online-DDL migrations minimal, ideally one ALTER per file. The
online_ddl keys, including fallback and args, are listed in
Configuration.
Next steps
Section titled “Next steps”- Which release lines are declared and at what support level: Database support matrix.
- Capability keys per dialect: Capabilities.
- The
online_ddlkeys and every other configuration key: Configuration.