Flow
Adding a name-picking helper, in order
1Write the two names in code
The list lives in one place and takes nothing from users.
2Check the table, in a fixed order
Return the first name the database confirms.
3Route the queries through it
Login, the update page, listing, saving and lookup by email.
4Paste only the name
Every value still goes through a placeholder.
5Remember hits, not misses
An upgraded database is noticed on the next request.
6Decide each caller's miss
Login errors, the update page falls back, setup refuses.
The login in the ordering system knew one name for the column it searches on. Some of my databases used a different name.
Emails in the user table are encrypted, so login can't search them. It searches a hash of the email instead, kept in its own column. Some of my databases name that column in camel case. Others use an underscore. Login only looked for camel case, and if it wasn't there, login stopped with a server error. On an underscore database, that's a broken login on a table that was fine.
Values fit in a placeholder, names don't
Using whichever name exists sounds easy, but look at where the name goes. A placeholder in a query can carry a value, like the hash you're looking for. It can't carry a column name. So when the column name changes, it has to be pasted into the SQL text. I don't like pasting text into SQL, so the name has to come from somewhere safe.
It comes from a short list written in the code. A small helper asks the database which of the two names the table really has, and returns that one:
KNOWN = ("lookupKey", "lookup_key")
The helper takes no input from the user. It can only ever return one of those two strings. That's what makes the pasting safe. If that list ever turns into a setting, or picks up anything a user typed, it isn't safe anymore.
Who uses the helper, and what happens on a miss
Nearly every query on the hash now asks the helper for the name: login, the update page, listing users, saving a user, and finding one by email. Only the name is pasted in. Every value still goes through a placeholder.
A good answer is remembered for the rest of the request. A miss is not, so the next call looks again. That way a database that gets upgraded is noticed on the next request.
When neither name exists, each caller does its own thing. Login logs it and returns a server error, which I'd take over guessing at a sign-in. The update page falls back to an older lookup, and I suspect that's on purpose, but it logs nothing when it does. First-run setup refuses to make the first admin if neither column is there, which is right, though it runs its own copy of the check rather than calling the helper.