A database does not receive your values. It receives a statement, as text, and parses it. Everything that happens after that is a consequence of what the parser found — and the parser has no idea which characters you wrote and which ones somebody sent you.
That is the whole of SQL injection, and it is worth sitting with for a moment, because almost every wrong mental model of this defect comes from imagining the database can tell the difference.
GET /orders/search?q=shippedAn ordinary request. Watch the statement the store parsed, in the transcript.
This build's search handler puts the term into the query with a bound parameter. Send a quote in it and nothing interesting happens: the quote arrives at the database as a character in a value, because the statement was already parsed before the value was bound.
Now send the same quote to a handler that built its statement by joining strings. The quote ends the literal early, and everything after it is parsed as SQL — clauses the handler never wrote, in a statement it thought it controlled.
GET /orders/search?q=%27%20OR%20%271%27%3D%271A quote through a bound parameter. It is a character, and it matches nothing.
GET /orders?status=x%27%20OR%20%271%27%3D%271The same characters, against a concatenated statement. Compare the two transcripts.
Two things a parameter cannot do, and both of them are where the remaining injections live. It cannot be a table name, and it cannot be a column name — so a handler that lets you choose a sort column has to interpolate it, and the only defence left is a list of the columns that route allows.
A semicolon is the other one worth knowing. This store parses every statement in the text it is given and records how many it found, so an input that added a second statement is visible as a number rather than as a guess about what the response looked like.
GET /orders?status=x%27%3B%20DELETE%20FROM%20notes%20WHERE%20%271%27%3D%271Look at the statement count in the transcript, not at the response.