Skip to content

Deleting a language leaves a runtime reference that breaks queries for the rest of the request #2322

Description

@ryancramerdesign

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions