Repository navigation
Conversation
|
Rebased onto master to resolve the conflict with #669: |
ac46558 to
a75026f
Compare
0c76f6a to
bb59bc0
Compare
|
Needs strict / careful review and/or discussion. |
1e0adbc to
7cfa545
Compare
|
Agreed, this one deserves a careful look. A summary for the review: What changes
Why not the issue's dual predicate (
Behaviour changes to check
How it is verified
Worth discussing
|
4da43cf to
67c32f0
Compare
|
@mawiesne Thanks for insisting on a careful look at the collation side. We checked the change against real servers: MySQL 8.0/8.4, MariaDB 10.6/10.11/11.4, PostgreSQL 17 and HSQLDB 2.7. On MySQL/MariaDB we tested each server's default collation plus utf8mb4_bin, utf8mb4_general_ci, utf8mb4_unicode_ci, utf8mb3_general_ci and latin1_swedish_ci. On PostgreSQL we tested the default and a nondeterministic ICU collation. We compared master with this branch rebased onto current master. What holds: the exact comparison now happens in Java, so What was wrong, and is fixed in the new commits on this branch, now rebased onto master:
Stored names with a trailing space no longer match: utf8mb4_bin pads spaces, Regression suite (the new
On performance: on utf8mb4 columns the old Out of scope, already on master: Open questions for you:
|
|
@mawiesne To check the change against real data rather than only our fixtures, could you run the following read-only script on one or two of your real MariaDB JWPL databases (ideally EN and DE) and attach the output? It only reads metadata and the name columns. Only the TSV at the end exports data, and it contains names only. -- 1. Server and schema defaults
SELECT VERSION(), @@character_set_server, @@collation_server,
@@character_set_database, @@collation_database;
-- 2. Charset/collation of the name columns and table sizes
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = 'name';
SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_ROWS
FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE();
SHOW CREATE TABLE PageMapLine;
SHOW CREATE TABLE Category;
-- 3. Plans and timings: old query (master) vs new query (#670), for a few titles
-- (e.g. 'Berlin', 'Köln', a multi-word title, a title with an emoji or CJK Ext-B if the wiki has one)
ANALYZE FORMAT=JSON SELECT name FROM PageMapLine WHERE name = 'Berlin' COLLATE utf8mb4_bin;
ANALYZE FORMAT=JSON SELECT name FROM PageMapLine WHERE name = 'Berlin';
ANALYZE FORMAT=JSON SELECT * FROM Category WHERE name = 'Berlin' COLLATE utf8mb4_bin;
ANALYZE FORMAT=JSON SELECT * FROM Category WHERE name = 'Berlin' ORDER BY id;
-- 4. Fan-out: how many rows the column collation treats as "equal"
SELECT cnt AS rows_per_name, COUNT(*) AS names
FROM (SELECT COUNT(*) AS cnt FROM PageMapLine GROUP BY name) t
GROUP BY cnt ORDER BY cnt;
SELECT name, COUNT(*) AS c FROM PageMapLine GROUP BY name ORDER BY c DESC LIMIT 20;
-- 5. Edge cases the PR changes
SELECT COUNT(*) FROM (SELECT BINARY name AS b FROM Category GROUP BY b HAVING COUNT(*) > 1) d; -- duplicate exact category names
SELECT COUNT(*) FROM PageMapLine WHERE name LIKE '% '; -- trailing spaces
SELECT COUNT(*) FROM PageMapLine WHERE name REGEXP '[^\\x{0000}-\\x{FFFF}]'; -- supplementary charactersAnd one small export: the names of the largest collision groups from step 4, which we would add as real-world cases to SELECT p.name, HEX(p.name) FROM PageMapLine p
JOIN (SELECT name FROM PageMapLine GROUP BY name HAVING COUNT(*) > 1 ORDER BY COUNT(*) DESC LIMIT 200) g
ON p.name = g.name
ORDER BY p.name;Why each part matters:
The GROUP BY queries scan the whole table once, so on EN they may take a few minutes. |
(A) => acceptable |
existsPage(String) and Category(Wikipedia, String) appended COLLATE utf8mb4_bin to their only name comparison, which keeps MySQL from using the name index and turns every lookup into a full scan. The queries now compare in the collation of the column and the exact, case-sensitive match is picked in Java from the few case variants returned. A dual predicate (plain plus collated comparison) was not used: MySQL 8.4 drops the collated one as redundant with the index lookup, so case variants matched, and a column-side COLLATE fails on utf8mb3 columns.
…atches as not found The category lookup by name now orders its candidates by id, so among categories of the same name the one with the lowest id wins on every engine, instead of whichever row the plan happened to return first. On MySQL and MariaDB, comparing a utf8mb3 or latin1 name column with a title the column charset cannot hold fails with "Illegal mix of collations" (errors 1267, 1270, 1271). The explicit COLLATE utf8mb4_bin used to avoid this. existsPage(String) and Category(Wikipedia, String) now treat the error as not found again, instead of throwing an unchecked exception.
The name lookups are tested against one variant database per collation profile of the name columns, next to the shared fixture: - HSQLDB: default and VARCHAR_IGNORECASE - MariaDB and MySQL: default, utf8mb4_bin, utf8mb4_general_ci, utf8mb3_general_ci and latin1_swedish_ci - PostgreSQL: default and a nondeterministic ICU collation NameLookupContractTest runs the cases of contract/name-lookups.tsv on each profile and compares them with NameLookupOracle, which computes the required outcome in Java. Known deviations are listed with their issue in contract/name-lookups-divergences.tsv; one that stops occurring fails as stale. NameLookupContractSpecTest checks the contract files without a database. NameLookupPlanTest replaces NameLookupIndexTest: it counts the rows the server reads for the SQL Hibernate really issued, and includes the 2.3.0 query as a control that must fail where it scanned. SchemaAssumptionTest pins the collations all of this relies on. @EnabledOnEngines names the engines a test runs on explicitly, so PostgreSQL no longer slips into a MySQL-only test.
8502d67 to
24abb7d
Compare
HSQLDB keeps the order of an existing index when ALTER COLUMN changes the type to VARCHAR_IGNORECASE, so lookups through name_index stayed case-sensitive and missed rows: getPageIdsCaseInsensitive found nothing, while getPage and the other lookups behaved as on a case-sensitive column. Drop and recreate the indexes around the type change, so the profile is case-insensitive throughout, and record the divergences it now shares with the case-insensitive MySQL and MariaDB collations.
|
@mawiesne Thanks for the answers on (A), (B) and (C). (A) and (B) are already implemented the way you decided: a collation mismatch (MySQL/MariaDB 1267/1270/1271, e.g. a title a Per (C), the follow-ups are filed as separate issues (milestone "Bug backlog"):
The divergence ledger ( One placeholder is gone without an issue: |
Wikipedia.existsPage(String)andnew Category(wiki, title)appendedCOLLATE utf8mb4_binto their name comparison on MySQL and MariaDB. With utf8mb3, latin1 or varbinary name columns, which include the legacyDEFAULT CHARSET=utf8layout, this kept MariaDB from usingname_index/nameIndex, and every lookup scanned the whole index. On utf8mb4 columns the index was already used:refon MariaDB, a range scan on MySQL 8.x. So the gain is mainly a MariaDB one on legacy charsets.The issue proposed a dual predicate (
name = :name AND name = :name COLLATE utf8mb4_bin). It did not hold up: MySQL 8.4 dropped the collated comparison as redundant with the index lookup, so case variants matched. Putting the collation on the column side fails on utf8mb3 databases ("COLLATION 'utf8mb4_bin' is not valid for CHARACTER SET 'utf8mb3'"). So this takes the issue's alternative.Changes
ß/ssor width variants of the title.SQL_COLLATIONis removed.CATEGORY_BY_NAME_QUERYorders byid, so duplicate exact category names resolve to the lowest id.COLLATE utf8mb4_binconverted the column first.Behaviour changes
uniqueResult()threwNonUniqueResultException.utf8mb4_binpads spaces,String.equalsdoes not.Titlenever produces such names, so only hand-imported data is affected.ß/ss, NFC/NFD, full-width, ignorable and supplementary-character variants give the same answers as master on every collation tested.Regression suite
This replaces
NameLookupIndexTest. That test was enabled with!= HSQLDBand would have failed on PostgreSQL (#727).contract/name-lookups.tsvis a readable table of 82 cases. The expected answers come from a plain-Java oracle using the productionTitle, not from the database under test.NameLookupContractSpecTestchecks the spec without a database.NameLookupContractTestruns the cases on every engine against several column collations. These are created as extra databases inside the existing CI containers, so there are no new matrix jobs:NameLookupPlanTestchecks the SQL Hibernate actually sends. On MySQL/MariaDB it counts rows read (Handler_read*); on PostgreSQL the JSONEXPLAINmust contain no Seq Scan. A control row runs the oldCOLLATE utf8mb4_binquery and must fail on MariaDB with utf8mb3/latin1 columns.SchemaAssumptionTestpins the default collations of the images and checks that each variant has the collation it claims.@EnabledOnEngines(...)makes tests list their engines explicitly, so a new engine cannot silently fall into an engine-specific test.Verification
dkpro-jwpl-apisuite:COLLATE utf8mb4_bin;order by id;Not in this PR
getPage(String),getPageIds(String),getCategories(String)and the LIKE-based lookups still follow the column collation, as on master. On a case-insensitive MySQL/MariaDB column,existsPage(x)can be false whilegetPage(x)returns a case variant. MovingPage.fetchByTitleto the same fetch-and-filter approach is a follow-up.Fixes #546.