Which parts of a dynamically composed query can be bound as parameters, and which must be chosen by your own code?
answer
- values bind, structure does not
- identifiers are text, not parameters
- map the request token to a fragment
- the caller's string is a key
- page size binds but still clamps
basics
~20 sPlaceholders bind values only. Column names, sort direction, comparison operators and the statement shape are text your code picks from a fixed internal map keyed by the request token. Page size and offset bind as values, but are still clamped to a ceiling.
solid answer
~50 sSplit the request into two kinds of input. **Values** - what to compare against, the elements of an in-list, the page size, the offset - go in as bound parameters, so their content can never turn into statement syntax. **Structure** - which column to filter or sort on, which direction, which comparison operator, which optional join appears - cannot be bound at all, because it decides the shape of the statement rather than filling a slot in it. So the code never interpolates the caller's string there. It looks the request token up in a fixed map it owns - `newest` maps to one specific column and direction - and emits what it finds; an unrecognised token is a rejected request. Paging is the case people miss: the number binds fine, but the builder still clamps it, because an unbounded page size hands a resource decision to the caller.
go deeper
Remember the split: values are bound as parameters, and anything that changes the shape of the statement - a column name, a direction, an operator - is chosen by the code instead.
Explain why a structural position cannot be bound: the parser needs it before the statement means anything, and a placeholder there is read as a constant or fails outright. Describe the token-to-fragment map as the mechanism.
Demonstrate the operational half: page sizes clamped and defaulted, ordering deterministic whenever a page is emitted, unknown tokens rejected, and tests that assert the emitted fragment and bind list rather than the response body alone.
Own the contract: which sort and filter tokens exist is a public API surface with a cost per entry, and mapping tokens to fragments is what keeps that surface independent of the schema underneath it.
## One boundary runs through the whole builder A dynamically composed query mixes two very different kinds of caller input, and almost every mistake in this area comes from treating them as one kind. - A **value** fills a slot in an already-decided statement: the right-hand side of a comparison, an element of an in-list, a row limit. A placeholder marks that slot, and the value travels to the engine outside the statement text, so nothing in it can be read as syntax. - **Structure** decides what the statement *is*: which column a predicate names, which column the result is ordered by, which direction, which comparison operator, whether an optional join is present at all. None of this can be bound, because there is no slot for it - the parser needs it before the statement means anything. A placeholder in a structural position does not fail loudly in every engine. Bound in an ordering position it is usually read as a constant, so the sort silently does nothing; bound where a column name belongs the statement typically fails to parse. Either way the caller's string never becomes an identifier just because a placeholder was pointed at it. ## What enters where | Request part | How it enters the statement | Chosen by | |---|---|---| | Filter value, range bound | Bound parameter | Caller | | Set membership elements | One bound parameter per element | Caller | | Page size, offset | Bound parameter, clamped first | Caller, capped by code | | Column filtered or ordered | Statement text | Code, via a token map | | Sort direction | Statement text | Code, via a token map | | Comparison operator | Statement text | Code, via a token map | | Which optional joins appear | Statement text | Code, from which filters arrived | ## The lookup table is the mechanism The practical form is dull and that is the point. The code owns a fixed map from **public tokens** to **statement fragments**: the token `newest` maps to one column plus a direction, `name` to another column plus a direction, an unknown token maps to nothing and the request is rejected. The caller's string is used as a **key**, never as content. Nothing about the string is inspected, cleaned or escaped, because it is never placed into the statement - only the fragment found under it is. That single property is what makes the structural half of the composition tractable. Trying to sanitise an identifier instead is a losing game: escaping rules are defined for string literals, quoting rules for identifiers differ between engines, and a check like *letters and underscores only* still admits a column the caller was never meant to order by. The reasoning about why a positive list is the only sound answer here belongs to input-validation and injection material; what matters for composition is the simple mechanical consequence - structural positions are filled from a table the code owns. Same mechanism, one step up: **which filters exist at all** is code-chosen too. A caller sends `status=open`; the builder decides that the token `status` corresponds to one predicate against one column with one operator. A generic *filter on any field with any operator* endpoint is the same design without the map, and it inherits every problem the map was there to prevent. ## Paging is a value with a ceiling Row limit and offset are ordinary values, and many engines accept them as bound parameters. Binding them keeps the statement text identical across page sizes, which is the whole point. But binding is not validating: 1. **Clamp the page size** in code to a maximum the caller cannot raise, and apply a default when it is absent. Otherwise one request asks for the whole table. 2. **Reject a negative or zero size**, rather than passing it through to whatever the engine does with it. 3. **Bound the offset** too, or replace deep paging with a cursor scheme; how to page efficiently is its own topic, but the composition rule is that the number arrives bound and pre-clamped. 4. **Always emit a deterministic ordering** when paging, or successive pages are not a partition of anything. ## The decoupling you get for free Because structural tokens are mapped rather than passed through, the public sort token and the physical column stop being the same name. Rename the column and you edit one map entry; expose a sort over a computed expression and the token maps to the expression. Callers never learned the schema, so the schema stays free to move - a benefit that arrives automatically once the caller's string is a key and not content. ## What to assert in tests - Each valid token emits the fragment it is meant to, and an unknown token is rejected rather than defaulted silently. - The bind list contains every value the request supplied and no structural token. - An oversized page size is clamped, and an absent one takes the default. - Paging without an ordering clause is impossible to express through the builder at all.
- Why can the elements of a set filter be bound, when the size of the set changes the statement?Each element is an ordinary value with its own placeholder; what changes with the size is only how many placeholders the builder emits. That is a structural decision the code makes from the list length, which is why a list filter produces one statement shape per distinct size - a small shape explosion worth capping.
- What is gained by mapping a public sort token to a column instead of exposing the column name?The caller's string becomes a key rather than content, so nothing about it needs cleaning, and the public contract stops tracking the schema. Renaming a column, ordering by an expression, or retiring a sort option each become a single edit to the map.
- Can the comparison operator be driven by the request at all?Yes, through the same map: the token `gte` selects one fixed fragment, `contains` another. What must not happen is the operator arriving as text to splice in. Keep the set of supported operators small and per-field, since not every operator makes sense for every column.
saying these in an interview costs you the question
- Thinks a placeholder can carry a column or table name if it is quoted.
- Strips suspicious characters from the sort token and uses the remainder.
- Accepts whatever page size the caller sends, with no ceiling.
- Binds the offset but pastes the row limit into the text.
- Exposes physical column names directly as the public sort parameter.
- Emits a paged query with no deterministic ordering clause.