The front page of atlasmuseum.net as the Internet Archive captured it on 28 September 2022, next to the same page as the wiki shows it on 11 September 2026. A seam runs between the two; whoever moves it sees the commands, queries and log lines of the migration.

1.26.4 to 1.39.10

  • PHP 7.4 to 8.3, MySQL 5.7 to 8.0
  • 43 extensions, each on PHP 8
  • database rescued, not rebuilt
  • 26 to 64 times faster on the measured pages

The lines in the seam, from the delivery document and the log files:

  1. MediaWiki: 1.26.4
  2. PHP: 7.4
  3. MySQL: 5.7
  4. am_archive.ar_rev_id: 149315 x 8000+ (duplicate)
  5. am_revision: index rev_timestamp missing (error 1176)
  6. update.php: a username was usable and an anonymous actor could not be created
  7. $ php maintenance/update.php
  8. $ php maintenance/cleanupUsersWithNoId.php --assign --prefix legacy --force
  9. $ php maintenance/cleanupUsersWithNoId.php --assign --prefix legacy --table revision --force
  10. $ php maintenance/cleanupUsersWithNoId.php --assign --prefix legacy --table oldimage --force
  11. $ php maintenance/deduplicateArchiveRevId.php --force
  12. SET @start := GREATEST(
  13. (SELECT IFNULL(MAX(rev_id),0) FROM am_revision),
  14. (SELECT IFNULL(MAX(ar_rev_id),0) FROM am_archive)
  15. ) + 1;
  16. SET @bad := 149315;
  17. UPDATE am_archive a JOIN tmp_dups t ON t.ar_id = a.ar_id
  18. SET a.ar_rev_id = IF((@i := @i + 1) = 1, a.ar_rev_id, (@n := @n + 1));
  19. $ php maintenance/sql.php --query "CREATE INDEX rev_timestamp ON am_revision (rev_timestamp);"
  20. $ php maintenance/populateContentTables.php
  21. INSERT IGNORE INTO am_updatelog (ul_key, ul_value) VALUES ('patch-change_tag-change_tag_rc_tag_id.sql','applied');
  22. $ php maintenance/rebuildrecentchanges.php
  23. $ php maintenance/runJobs.php --maxjobs 500
  24. $ mysqldump wiki --single-transaction --routines --triggers > wiki_after_mw139.sql
  25. $ php extensions/SemanticMediaWiki/maintenance/rebuildData.php
  26. $ php extensions/SemanticMediaWiki/maintenance/updateEntityCollation.php
  27. $ php maintenance/rebuildLocalisationCache.php --force
  28. $ MW_NO_EXTENSION_HOOKS=1 php maintenance/importImages.php --overwrite --search-recursively images_clean
  29. $ php maintenance/rebuildImages.php
  30. $ php extensions/AtlasQualite/maintenance/rebuildQualityIndex.php
  31. $ php extensions/AtlasRepair/maintenance/repairFromWikidata.php
  32. $ php extensions/AtlasRepair/maintenance/repairFromWikidata.php --apply
  33. "manifest_version": 2,
  34. "requires": { "MediaWiki": ">= 1.39.0" },
  35. "AutoloadNamespaces": { "WikidataBundle\\": "includes/" },
  36. execute(): signature aligned with SpecialPage in 1.39
  37. trim(null) -> trim((string) $value)
  38. str_replace on null -> coalesced to empty string
  39. string concatenation: + replaced by .
  40. duplicate class definitions removed, autoloader regenerated
  41. Semantic MediaWiki: 5.1.0
  42. MediaWiki: 1.39.10
  43. Storage engine: SMW\SQLStore\SQLStore
  44. ... checking outdated entities ... ok
  45. ... checking invalid entities by namespace ... ok
  46. ... refreshed (IDs) 51277
  47. ... skipped (IDs) 0
  48. finished: 2026-09-09 02:03:28 CEST exit=0
  49. [10:05:37] Map rebuilt: 42713 points written to am_map in 1771 ms.
  50. PHP 7.4 -> 8.1 -> 8.3
  51. MySQL 5.7 -> 8.0
  52. MediaWiki 1.26.4 -> 1.31 -> 1.35 -> 1.39.10
  53. repeated page load: 26 to 64 times faster
  54. memoisation within one render + 30 minute disk cache
  55. loopback removed: the server no longer called its own web API

2025 to 2026 · MediaWiki · A-Pack, France

The job nobody wanted

In October 2025 I was called about a site that was broken. Atlasmuseum is a public atlas of public art in France, run by the association A-Pack for the French Ministry of Culture.

1.26.4 to 1.39.10Version path, in stages
26 to 64 timesFaster on repeat page loads
43Extensions carried to PHP 8.3
400 hoursA large share of it personal time

Prove it first, quote afterwards

The live site had been compromised by pirate accounts and was running MediaWiki 1.26.4, released in 2015. A previous migration attempt had been abandoned halfway and left the database with mixed and missing tables.

More than one party had looked at it and said it could not be saved, and that starting again would be the sensible thing.

I did not say yes or no straight away. From the client’s database dump I rebuilt the whole site in a fully offline environment on my own machine, with no connection to the live site and therefore no risk. Only once that test showed the content could be saved did a plan and a quote follow.

That is how I work. I do not sell certainty I do not have yet.

The work was in the database

The path ran from MediaWiki 1.26.4 to 1.31, to 1.35, to 1.39.10, with MySQL from 5.7 to 8.0 and PHP from 7.4 through 8.1 to 8.3. But the version numbers are not the work.

Over 8,000 duplicate archive revisions renumbered, on revision 149315 alone. A missing revision timestamp index restored. The actor model migration completed. The Multi Content Revision tables, the logging, the change tags, the file archive and the user talk pages repaired one by one.

Tens of thousands of spam pages removed, with the deletion list reviewed three times before anything went, because you do not get back what you throw away by mistake.

Then the content. On the order of 100,000 semantic entities regenerated. Over 4,200 images re-imported with their metadata rebuilt, after the media backend turned out to be hanging off the wrong tables.

A rescued site that is slow is only half rescued

Once all of that stood, the site was still slow. Repeat page loads ended up 26 to 64 times faster than the starting point, on the pages that were benchmarked.

That did not come from a trick. A two level cache around the upstream API and the SPARQL calls: memoisation within one page render, plus an on-disk cache that keeps for thirty minutes. Parser and page cache switched on, with a cache epoch to force a clean redraw without turning the cache off. Heavy galleries behind an IntersectionObserver.

And the removal of a construction where the server called its own web API. That one was slow, and under load it could exhaust the whole worker pool.

The third thing: six tools that did not exist

A migration is finished when the site runs. I did not think that was enough. I built six tools, and they all have the same purpose: that the platform keeps itself clean once I am gone.

AtlasQualite scans the whole catalogue, about 4,600 artwork notices, and scores each one for completeness against the fields that matter. Image, coordinates, author, date, description, Wikidata link. The result is a prioritised worklist for the editors. You cannot improve what you do not measure.

AtlasRepair clears up what old imports left behind. The pattern is deliberate: first a dry run that changes nothing and writes a report, then an overview with every proposed change from current to corrected value, and only then an application with attribution and a rate limit. On the last dry run it flagged 1,343 records.

Wikidata Enrich fills empty fields in one click, and only empty fields, always with a preview before anything is saved. AMAPIStatus calls the self test and shows per endpoint whether it works. For a platform being handed to a public institution, being able to point at a page where everything is green is worth a great deal.

MapSync rebuilds the map automatically on every save or import, in process, transactionally, with its own lock so two rebuilds cannot collide. AtlasNova is the new skin: 1,478 lines against 3,527 in its predecessor. Less than half, for something that is more modern and easier to maintain.

Together I wrote over 27,000 lines of my own code, inside an installation of more than two million lines and 43 extensions that each had to come along to 1.39 and to PHP 8.3.

The rules I held myself to

This is the part a client can use. Every tool that writes is restricted to logged in editors. Nothing anonymous can start a change.

Bulk operations always run dry first and only write after a person has read the report. There is no path where a mass edit happens without someone confirming it. Every file I edited was backed up first with a timestamp, so each individual change can be rolled back. Error display is off for visitors and on in the logs for the administrator.

And one design rule that carries the rest: my own API layer reads MediaWiki only through the stable action API and never directly from SQL. A future upgrade therefore cannot break the backend. That is exactly what went wrong in the installation I inherited.

What is still open

The project document also keeps track of what is not finished. The map table holds 988 rows where 4,537 have been in it, because a batch that timed out was once read as no more results. The guard against that truncation is in place now, so a rebuild is safe. And the coordinate property is an ordinary page property rather than a geographic one, so the radius search over our own data returns nothing.

I delivered a sixty page runbook with it, containing the full error catalogue and the open points. Not because I had to, but so the next developer does not have to work out again what I already know.

Work