Repository navigation
Charset and collation support #456
Description
Problem
The driver parses SQL as UTF-8, and SQLite stores all text as UTF-8. MySQL character sets aren't currently mapped onto that. Connection charset settings are ignored, and declared charsets are recorded but never enforced. We need to declare and implement sensible support.
Charsets
MySQL operates with two charsets—storage charset and connection charset. In PHP, SQLite uses UTF-8 for both. In the SQLite driver, the connection charset will be UTF-8. For storage, we can also support single-byte charsets by checking their values on write and handling them correctly in special cases (e.g., when their binary values are used).
Let's declare character set support as follows:
1. Unicode charsets: Support all of them.
utf8mb4, utf16, utf16le, utf32 and gb18030 accept every Unicode character. utf8mb3 (utf8) and ucs2 accept only the Basic Multilingual Plane, which excludes most emoji. The check is "anything" or "code point ≤ U+FFFF".
2. Single-byte charsets: Support the most common ones. Start with ascii and latin1.
These are 25 single-byte charsets in total: ascii, latin1, latin2, latin5, latin7, cp1250, cp1251, cp1256, cp1257, cp850, cp852, cp866, koi8r, koi8u, greek, hebrew, tis620, armscii8, geostd8, dec8, hp8, keybcs2, macce, macroman and swe7. Each covers at most 256 characters, so a list of 256 characters per charset is enough. We could generate those lists from the mapping files in MySQL's source, which keeps them exact, including quirks like latin1 really being Windows-1252. That's a few KB of data in total.
We should start with ascii and latin1 and add other ones as needed.
3. Other multi-byte charsets: Reject.
Reject charsets like big5, gbk, gb2312, sjis, cp932, ujis, eucjpms and euckr. Support for some via mbstring may be considered in the future.
4. Binary: Support using SQLite binary types.
Connection
The connection charset is always UTF-8. Connection settings must reflect that as follows:
- Accept
utf8mb4andutf8mb3/utf8in:SET NAMES(SET NAMES DEFAULTmeansutf8mb4);SET NAMESaccepts an optionalCOLLATEclause. See the Collations section.SET CHARACTER SET;character_set_client,character_set_connection, andcharacter_set_resultsvariables (character_set_resultsalso acceptsNULLandbinary= "no conversion");- DSN
charsetoption.
- Reject any other charset with a clear error.
- Report the charset in
@@character_set_client,@@character_set_connection,@@character_set_results,@@collation_connection, andSHOW VARIABLES. - Input text must be valid UTF-8. Invalid byte sequences are handled like MySQL handles them for a
utf8mb4client, instead of being passed to SQLite as is. - WordPress
$wpdb->charsetis alwaysutf8mb4. A non-UTF-8DB_CHARSETorDB_COLLATEis ignored and reported (e.g., in Site Health), not treated as a fatal error.
Collations
Collations can be addressed separately. Until then, all text columns keep using COLLATE NOCASE.
The support should be implemented as follows:
- Metadata: Record and report collations like charsets. Resolve them from column, table, and database defaults, validate them against the charset, and let columns inherit them. List all collations of supported charsets in
INFORMATION_SCHEMA.COLLATIONSandSHOW COLLATION. - Connection collation:
SET NAMES ... COLLATE ...setscollation_connection(withoutCOLLATE, the default collation of the charset). Validate it against the connection charset, report it in@@collation_connectionandSHOW VARIABLES, and apply it to literals and other values that don't come from a column. - Behavior: Map collations to built-in SQLite collations by their suffix and pad attribute:
_bin:RTRIMforPAD SPACEcollations (e.g.,utf8mb4_bin), andBINARYforNO PADones (e.g.,utf8mb4_0900_bin). Exact for Unicode charsets._cs,_as_cs,_ks:BINARY. Equality is close, but ordering follows code points instead of language rules._ci,_ai_ci,_as_ci:NOCASE. Case folding is ASCII-only, accents are always significant (only right for_as_ci), and so are trailing spaces.- Language-specific collations (e.g.,
utf8mb4_czech_ci,utf8mb4_tr_0900_ai_ci) map by their suffix. Their language rules are not emulated.
- No rejection: Collations that can't be matched exactly are accepted and mapped as above. Unlike charsets, they don't change stored data, only comparisons, ordering, and uniqueness. The common defaults (
utf8mb4_unicode_520_ci,utf8mb4_0900_ai_ci) can't be matched exactly either, so rejecting them would break almost every site. - Reporting: Report tables and columns whose collations are not emulated exactly (e.g., in Site Health).
- Known gaps: SQLite has no Unicode case folding, accent-insensitive, or language-specific collations, and no case-insensitive collation that ignores trailing spaces. Custom collations registered in PHP could close some of these gaps, but other SQLite tools then fail on tables that use them ("no such collation sequence").
- Default:
utf8mb4defaults toutf8mb4_0900_ai_ci, like in MySQL 8. Changing that is a separate decision.
Other expected behavior
- Unsupported charsets (including single-byte ones not added yet) fail with a clear error in
CREATE/ALTER TABLEandCREATE/ALTER DATABASE. Unknown names fail with MySQL error 1115. - No rewriting: Supported charsets are kept as declared and validated, not rewritten (e.g.,
utf8mb3is not rewritten toutf8mb4). - No configuration option: There is no strict or lenient mode. Unsupported charsets always fail, and supported ones are always validated.
- Defaults resolve like in MySQL: column, then table, then database.
DEFAULTmeans the database default, and columns without a charset inherit the table charset. utf8is an alias ofutf8mb3, reported asutf8mb3when emulating MySQL 8.0.30+.CHARACTER SET binaryturnsCHAR,VARCHAR, andTEXTintoBINARY,VARBINARY, andBLOB.- Writes (
INSERT,UPDATE,REPLACE) fail with MySQL error 1366 in strict mode when a value doesn't fit, and store what MySQL stores otherwise. ALTER TABLEchanging a column charset checks existing values.ALTER TABLE ... CHARACTER SETchanges the table default.- Conversions:
CONVERT(... USING ...),CAST(... AS CHAR CHARACTER SET ...), and introducers (_latin1'...') apply the target charset (?for characters it can't represent). This also makeswpdb::strip_invalid_text()work. - Byte-level functions (
LENGTH(),HEX(),CAST(... AS BINARY)): decide which return the bytes of a non-UTF-8 declared charset. SHOW CREATE TABLEprints columnCHARACTER SETandCOLLATEwhen they differ from the table default, so exports keep them.INFORMATION_SCHEMA.CHARACTER_SETSandSHOW CHARACTER SETlist the supported charsets.- Existing databases: Already recorded unsupported charsets (e.g., a
big5column) keep working without checks, and are reported (e.g., in Site Health). - Behavior change: New non-
utf8mb4columns start rejecting characters accepted today (e.g., emoji inutf8). Document it.
To consider: Before releasing, run a plugin test set (e.g., Playground Tester) to measure the impact of the new errors and write checks.
Activity
[-]Support non-UTF-8 character sets in CONVERT expressions[/-][+]Limit character-set support to UTF-8, ASCII, and binary[/+][-]Limit character-set support to UTF-8, ASCII, and binary[/-][+]Charset and collation support[/+]Fix `utf8mb4_0900_ai_ci` collation in tables created by WordPress (#514)