"because not every input to every query can be bound as a variable" Can you give me an example?
var sql = "select * from User where UserType=@ut order by ModificationDate " + sort_order;
In this case, if sort_order directly comes from a 'asc'/'desc' radio button then you have an injection attack.
The correct way to do it would be:
var sql = "select * from User where UserType=@ut order by ModificationDate " + (sort_order=="asc"?"asc":"desc");
The point was that there are some parts of sql that can't be parameterized like sort order or limits on the resultant recordset. Although, ideally support for those things should be coming from your database vendor (if it isn't already there).
For example, SqlServer supports using variables in top expressions: select top(@max) * from ....
Because when you try to use a prepared statement to populate a paginated table, you still force the database to construct a dynamic query based in part on user inputs when you pass the OFFSET and LIMIT to it.
Recommended exercise: implement the MySQL wire protocol. It took me a couple hours a few years ago. It's not hard. Do it, come back, and then see if you still think the same things about SQLI defenses.
"because not every input to every query can be bound as a variable" Can you give me an example?
"Some operations do require dynamic query construction." what does that have to do with prepared queries?