Audits every civicrm_* InnoDB table in the CiviCRM database (and, while
detailed logging is enabled, its log tables) for legacy utf8 / utf8mb3
collations, shows exactly what would change (per column), and converts
selected tables to utf8mb4 through a queue with progress tracking and a
permanent audit log.
Tables still on 3-byte utf8 cannot store emoji or other 4-byte characters —
inserts fail with Incorrect string value errors. CiviCRM core ships a blunt
System.utf8conversion API; this extension is the careful, auditable
alternative:
- Per-column fidelity — preserves
_bincollations (hash columns stayutf8mb4_bin), NULL/NOT NULL, defaults,auto_increment, and comments. Literal and expression defaults are told apart by howSHOW CREATE TABLEwrites them, and literals and comments are escaped by the database connection, so quotes and backslashes survive unchanged. Backtick-quoted, ANSI-quoted and unquoted column names inSHOW CREATE TABLEare supported. If a required definition cannot be read or parsed, the table is marked Blocked and no ALTER is generated or executed. - Generated columns handled correctly — redefined with their generation
expression so they convert along with their base columns. (Skipping them
leaves the table unable to accept 4-byte input.) The expression is read
from
SHOW CREATE TABLE, because MySQL 8'sinformation_schemareturns it with its quotes escaped a second time. - Dry run by default — the analysis page never touches the database. Review the exact ALTER statement for every table before anything runs.
- Pre-flight warnings — large tables (lock duration), and indexes whose
key would exceed the InnoDB limit once their columns take 4 bytes per
character (the ALTER would fail). The limit accounts for row format,
innodb_large_prefixandinnodb_page_size. Composite keys include numeric, date, binary, ENUM and SET components; unknown or functional key parts produce a manual-verification warning. - Row format — converted InnoDB tables move to
ROW_FORMAT=Dynamic, exceptCompressedtables, which keep their compression. - Server context at a glance — database name, MySQL/MariaDB flavor and
version, and the server, database and connection variables that matter
for a safe conversion (
character_set_server,collation_server,character_set_database,collation_database,character_set_connection,collation_connection,innodb_file_per_table,innodb_large_prefix,innodb_default_row_format,innodb_page_size,max_allowed_packet) are shown right on the analysis page, each with a help icon explaining what it means for the conversion. - Detailed-logging tables — while CiviCRM's detailed logging is enabled,
the log tables are analyzed and converted too, in the same database or in
the separate one named by
CIVICRM_LOGGING_DSN. See Detailed logging. - Scope report — the analysis page lists CiviCRM tables this tool
doesn't convert that still use 3-byte utf8: non-InnoDB
civicrm_*tables, log tables while logging is disabled or on an engine other than InnoDB or ARCHIVE, andlog_civicrm_*tables left in the CiviCRM database when logging uses a separate one. - Queue-based execution — one table per task via
CRM_Queuewith a progress bar. A single large ALTER can still exceed a web request timeout; use the CLI for large tables and inspect any interrupted audit records. The target collation shown on the confirmation screen is pinned for the whole run, even if the setting changes while it is in progress. The confirmation screen saves the reviewed SQL and a schema fingerprint. Changes are checked before queue creation and again by each task; changed tables require a fresh review. Ordinary AUTO_INCREMENT counter movement does not invalidate the review. Existing queued tasks without a saved plan are refused and must be reviewed again. - Audit log — every statement, its duration, before/after collation,
outcome, and the operator are recorded in
civicrm_collationfix_log. Arunningrecord is written before each ALTER and updated afterwards, so an interrupted request still leaves a trace. After each ALTER the table is re-analyzed; any column (or table default) still off the target collation, or a re-analysis that fails, marks the run as an error. - Command-line API —
Collationfix.convertconverts one or more tables fromcv,drushorwp-cli, so large tables are never cut off by a web request timeout. Same verification and audit log as the UI. - System status check — warns on the CiviCRM status page while any table default or column remains on 3-byte utf8, counting log tables while detailed logging is enabled.
Works with MySQL 5.7 / 8.x (utf8_* and utf8mb3_* collation names) and
MariaDB.
This is an extension for CiviCRM, licensed under AGPL-3.0.
- Install and enable the extension.
- Go to Administer → System Settings → Collation Fix (utf8mb4) or visit
civicrm/admin/collationfix. - Optionally pick the target collation (default
utf8mb4_unicode_ci) at Administer → System Settings → Collation Fix Settings (civicrm/admin/setting/collationfix) or via the change link on the analysis page. The list shows the non-binaryutf8mb4collations your server supports;_bincolumns always becomeutf8mb4_bin. - Review the analysis: expand any table to see the per-column diff and the exact ALTER statement. Warnings are shown for large tables and risky indexes.
- Select tables, then either click Download selected as .sql to run the statements manually, or Review and convert selected tables.
- On the confirmation screen, type the database name to enable execution. The conversion runs as a queue with a progress bar and lands on the conversion log when done.
When detailed logging is enabled (Administer → System Settings → Misc →
Logging), triggers copy every change to a CiviCRM table into its log table,
log_civicrm_*. A table converted to utf8mb4 then passes 4-byte characters to
its trigger, and a log table still on utf8 rejects them: the whole save fails
with Incorrect string value (or, without strict SQL mode, the log stores
?). So a table is only fully converted once its log table is too.
- Only while logging is enabled. Log tables are listed and converted only
while the
loggingsetting is on. With logging off nothing writes to them; any still on utf8 are shown in the scope report instead, because they are reused if logging is turned on again. - Which tables. The log tables CiviCRM itself knows about
(
CRM_Logging_Schema):log_civicrm_*plus those of tables added throughhook_civicrm_alterLogTables. InnoDB and ARCHIVE log tables are converted; ARCHIVE tables keep their engine, and get a warning because their ALTER rewrites the whole table. To move them to InnoDB first, runcv api System.updatelogtables forceEngineMigration=1. - Separate logging database. When
CIVICRM_LOGGING_DSNnames another database, its log tables are shown and passed asdatabase.table(for examplecivicrm_logs.log_civicrm_contact) on the analysis page, in the downloaded SQL, in the API and in the audit log. Like CiviCRM core, the tool reaches that database through the CiviCRM connection, so the CiviCRM database user needsALTERon it; without it the ALTER fails, changes nothing and is recorded as an error. The confirmation screen then also asks for the logging database name. - Convert both together. Every table that needs conversion warns when its log table needs it too, and the confirmation screen lists selected tables whose log table is not selected. Log tables are always converted first, which is safe: a log table on utf8mb4 accepts everything a utf8 table sends it.
- Locking. While a log table's ALTER runs, saves to the table it logs wait for it, because the trigger writes to the log table. Log tables are often the largest tables in the database; convert them from the command line, in a quiet period.
For large tables, convert from the command line instead of the browser. The
Collationfix.convert API (APIv3) runs the tables one after another in the
same process, with the same analysis, post-conversion verification and audit
log as the UI queue.
# Preview the ALTER statements without running them.
cv api Collationfix.convert tables=civicrm_contact,civicrm_activity dry_run=1
# Analyze every table (read-only), e.g. for monitoring.
cv api Collationfix.convert dry_run=1
# Convert. Pass --user so the audit log records who ran it.
cv --user=admin api Collationfix.convert tables=civicrm_contact,civicrm_activity
# Same API from drush (Drupal) or wp-cli (WordPress).
drush civicrm-api Collationfix.convert tables=civicrm_contact
wp civicrm api Collationfix.convert tables=civicrm_contacttables— one or morecivicrm_*InnoDB tables, or log tables while detailed logging is enabled (database.log_civicrm_*for a separate logging database), as a comma-separated string or an array. Required unlessdry_runis set. Every name is checked before anything runs; an unknown name stops the whole call. Log tables are converted first.dry_run— return each table's generated statement, columns and warnings without executing anything. Withouttables, every table is analyzed. Results also includeblocked,errors, andschema_fingerprint. Blocked tables have an empty statement and remain marked as needing work.- All tables in one call convert to the target collation read at its start.
- If any table fails, the call returns an error (non-zero exit from
cv) that still includes every table's result. The other tables are still converted, as in the UI queue.
Take a database backup before converting. Each ALTER rebuilds and locks its table; large tables can take several minutes and will cause replication lag on replicated setups.
- Operates on the database
CIVICRM_DSNconnects to, and only on tables whose names start withcivicrm_, plus the detailed-logging tables while logging is enabled (see Detailed logging). On a shared Drupal/WordPress database the CMS's own tables (users,node,sessions, etc.) are not listed or converted. Neither are extension tables that use another prefix (e.g. CiviRules'civirule_*). Convert those separately if needed. - Views are never listed or altered, including the
civicrm_<table>_<locale>views (e.g.civicrm_option_value_fr_FR) that multilingual CiviCRM creates. A view's columns take their character set from its base table, so convertingcivicrm_option_valueconverts what its locale views return; they need no rebuild. - Only analyzes
civicrm_*tables using theInnoDBengine, and log tables usingInnoDBorARCHIVE; tables on other engines (e.g.MyISAM/Aria) are not converted, even if they are still on a legacy utf8 collation. They are listed in the analysis page's scope report. - Assumes the default SQL mode handling of backslashes. Like CiviCRM core,
its
LIKE 'civicrm\_%'table filters find nothing underNO_BACKSLASH_ESCAPES. - Converts the utf8 family only (
utf8,utf8mb3, and normalizes otherutf8mb4_*variants to the configured target collation); latin1 and other charsets are left untouched. - Schema conversion only — it does not repair mojibake or double-encoded data.
- Schema checks detect changes since review; they do not lock the schema between the final check and ALTER. Avoid concurrent schema migrations.
- Index-size checks are preflight warnings, not full DDL validation. Foreign key compatibility and unique-value collisions still need separate review.
NOT NULLis not re-emitted on generated columns (MariaDB rejects the clause on generated columns); such columns are rare in CiviCRM schemas.
Run php tests/regression.php from the extension directory. These standalone
tests simulate CiviCRM and database responses and never connect to a database.
They cover SQL parsing, blocked tables, schema drift, queue/form review state,
audit ordering, verification failures, API results, composite index sizes,
and detailed-logging tables (logging on/off, separate logging database,
ARCHIVE log tables, conversion order and the scope report).
They complement, rather than replace, conversion tests on disposable MySQL
and MariaDB databases.