Short description
After a language is deleted, a query can still be built against that language's pages.name{id} column, which no longer exists:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'pages.name18480' in 'order clause'
This surfaces reliably in the core test suite: php index.php test all fails in the Users test, while php index.php test Users on its own passes. LanguageSupport.test.php creates and deletes a temporary language earlier in the same process, and something continues to reference it afterwards.
Partly addressed already
One concrete case is fixed in b80c39dc: the current user was left with the deleted language still set, so anything sorting or matching on a page name afterwards referred to a dropped column. That is what an administrator would hit by deleting the language they are currently browsing the admin in. It self-heals on the next request.
That fix is verified, but it does not resolve the test suite failure, so at least one other runtime reference survives a language deletion.
What the remaining problem is not
Worth recording, to save the next person the same search:
- It is not persistent state. After a failing run:
pages_sortfields has no matching rows, no template data references the language, and only the default language remains. The name{id} column is correctly dropped.
- It is not the static caches that
LanguageSupport.test.php currently scrubs by reflection — PageProperties::$languageProperties, Fields::$nativeNamesLocal, and PageFinder/PageFinder2::$pagesColumns. Those are populated from module init() at boot, so a language created mid-request is never registered in them at all. Confirmed by checking $fields->isNative("name$id") while the test language existed: already false. Those four reflection workarounds in the test may therefore be unnecessary.
By elimination, the reference is held by something reading the live languages list at query time.
Related gaps found while investigating
Languages::hookUnknownColumnError() is the designed recovery point for this class of error, hooked in the Languages constructor. It handles the inverse case only:
foreach($this as $language) {
if($language->id != $languageID) continue; // no match when the language was deleted
... recreate the missing column ...
}
When a language exists but its column is missing, it recreates the column. When the language has been deleted, nothing matches and it silently does nothing.
The query is not retried. At wire/core/WireDatabase/WireDatabasePDO.php:1003, a 42S22 error calls that hook but does not retry, unlike the HY000 branch immediately below it which does. So even when the hook does recover, it only helps subsequent queries, never the one that failed.
LanguageSupportPageNames::languageDeleted() drops the columns and index but removes nothing that was registered about them at runtime. Its counterpart languageAdded() is likewise schema-only. Relatedly, Fields::setNative() has no removal counterpart at all.
Impact
Bounded but real. Deleting a language breaks page queries for the remainder of that request, and recovers on the next one. The main practical cost today is that it makes test all fail in an unrelated test, which is misleading.
Suggested direction
Find what holds the language reference at query time, then decide whether the fix belongs there or in giving hookUnknownColumnError() a branch for "column belongs to a language that no longer exists". Making 42S22 retry after successful recovery would also make the existing self-healing actually work for the query that triggered it.
Environment
- ProcessWire dev (3.0.271), reproduced on the standard test install
- MySQL 8.0.44
-Claude
Short description
After a language is deleted, a query can still be built against that language's
pages.name{id}column, which no longer exists:This surfaces reliably in the core test suite:
php index.php test allfails in theUserstest, whilephp index.php test Userson its own passes.LanguageSupport.test.phpcreates and deletes a temporary language earlier in the same process, and something continues to reference it afterwards.Partly addressed already
One concrete case is fixed in b80c39dc: the current user was left with the deleted language still set, so anything sorting or matching on a page name afterwards referred to a dropped column. That is what an administrator would hit by deleting the language they are currently browsing the admin in. It self-heals on the next request.
That fix is verified, but it does not resolve the test suite failure, so at least one other runtime reference survives a language deletion.
What the remaining problem is not
Worth recording, to save the next person the same search:
pages_sortfieldshas no matching rows, no template data references the language, and only the default language remains. Thename{id}column is correctly dropped.LanguageSupport.test.phpcurrently scrubs by reflection —PageProperties::$languageProperties,Fields::$nativeNamesLocal, andPageFinder/PageFinder2::$pagesColumns. Those are populated from moduleinit()at boot, so a language created mid-request is never registered in them at all. Confirmed by checking$fields->isNative("name$id")while the test language existed: alreadyfalse. Those four reflection workarounds in the test may therefore be unnecessary.By elimination, the reference is held by something reading the live languages list at query time.
Related gaps found while investigating
Languages::hookUnknownColumnError()is the designed recovery point for this class of error, hooked in theLanguagesconstructor. It handles the inverse case only:When a language exists but its column is missing, it recreates the column. When the language has been deleted, nothing matches and it silently does nothing.
The query is not retried. At
wire/core/WireDatabase/WireDatabasePDO.php:1003, a42S22error calls that hook but does not retry, unlike theHY000branch immediately below it which does. So even when the hook does recover, it only helps subsequent queries, never the one that failed.LanguageSupportPageNames::languageDeleted()drops the columns and index but removes nothing that was registered about them at runtime. Its counterpartlanguageAdded()is likewise schema-only. Relatedly,Fields::setNative()has no removal counterpart at all.Impact
Bounded but real. Deleting a language breaks page queries for the remainder of that request, and recovers on the next one. The main practical cost today is that it makes
test allfail in an unrelated test, which is misleading.Suggested direction
Find what holds the language reference at query time, then decide whether the fix belongs there or in giving
hookUnknownColumnError()a branch for "column belongs to a language that no longer exists". Making42S22retry after successful recovery would also make the existing self-healing actually work for the query that triggered it.Environment
-Claude