A search endpoint returned 500 on four queries out of nine hundred and fifty real ones. The error on one of them:
That reads like a schema problem. Something is asking for a column called 8, so look at the table definition, look at the migration, look at whatever builds the SELECT.
None of that is where it is. The 8 came from a user.
What the user typed
They had pasted a fragment of an HTML page into the search box. Somewhere in it:
The search runs on SQLite's full-text extension. The term goes in as a bound parameter, which is what everyone is told to do, and the code wraps it in double quotes so that the whole thing is treated as one phrase rather than a bag of words.
Binding a parameter protects the SQL statement. It does not protect the value from the engine that reads it next. The full-text extension takes its argument and parses it again, in its own query language, where a double quote opens and closes a phrase.
Hence no such column: 8. The message is accurate. It is describing a column reference that the user's paste created and that nobody in the codebase wrote.
Three shapes, one cause
Five of the nine hundred and fifty queries contained a double quote. All five throw at the search layer. What they throw depends on where the quotes land.
| quotes | error |
|---|---|
| one | unterminated string |
| two | syntax error near a token from the middle of the query |
| four | no such column |
There is no rule about odd or even. Any double quote at all breaks the phrase, and the count and position only decide which way the wreckage lands. Two of the three messages look like parse errors, which is at least honest. The third looks like a schema bug and sends you to the wrong file.
Counting it honestly
Four became 500 at the endpoint. Five throw when the search layer is called directly. The fifth does not surface as an error, and why it does not was never traced; something upstream apparently drops that term or takes another path, but that is a guess and it has not been checked.
So the accurate statements are: four of nine hundred and fifty real queries returned 500, and all five quote-containing queries throw at the component level. The tempting summary, that a quote breaks search, is contradicted by one case out of five, and the reason that case is different is unknown.
That gap is small and it is the kind that quietly becomes a false rule if the two numbers get merged into one sentence.
The fix
Doubling a quote is how you put a literal one inside a phrase. One line, no schema change, no migration.
Finding it took a day. Fixing it took a line. That ratio is the normal one for this class of bug, and it is the reason the error message mattered so much: two thirds of the day went to the word "column."
Proving the check works
After the fix, the natural move is to run the nine hundred and fifty queries, watch them all pass, and call it done.
That confirms the code and not the check. A test suite that has stopped exercising the failing path also passes.
So the fix was reverted on purpose and the same sweep run again:
Now the green result means something, because the red result was produced on demand from the same harness. A passing gate is only evidence if you have seen it fail.
What to take from it
Parameter binding solves exactly one problem, which is the SQL statement being rewritten by its own inputs. If the bound value is then handed to something with its own grammar, a full-text query language, a glob, a regular expression, a template, the input is unescaped again for that grammar and nobody mentioned it.
The general question is not whether input is parameterised. It is how many parsers the input passes through, and whether it has been escaped for each of them.