📎Parameters and prepared statements
Shape once, send values many times
Prepared statements send SQL text with placeholders, then ship parameter values on the wire separately from the parse tree, blocking classic injection and often letting the server reuse parse/plan work. Identifiers (table/column names, ORDER BY keys) are still not parameters; whitelist or map them explicitly.
PostgreSQL PREPARE / EXECUTE (logical view)
PREPARE get_run (text) AS
SELECT run_id, started_at FROM sequencing_runs WHERE instrument_id = $1;
EXECUTE get_run('SEQ-01');| run_id | started_at |
|---|---|
| R-900 | 2026-04-10 08:00 |
| R-901 | 2026-04-11 09:12 |
âš¡ Advanced / dialect note
Parameters mistaken for every token
- Interpolating
ORDER BYcolumn names or sort direction from user input. - Client-side quote escaping instead of driver-level binding.
- Assuming one prepared statement survives all pooled connections without lifecycle rules.
Bind values, not identifiers
- Whitelist sort columns in application code; use enums or maps for directions.
- Monitor plan cache churn and single-use plans in production telemetry.
- Teach the difference between bind parameters (values) and identifiers (names): only the former are safe from concatenation.
🔌Protocols
Extended query vs simple query mode.
Drivers
Stored procedures
PREPARE does not absolve string-built EXEC inside the body.ORMs
ORM escape hatches that accept raw SQL strings are the usual leak, code review should treat them like eval with a WHERE clause.
TL;DR
Prepared statements send SQL text once with placeholders ($1, ?) and bind values per execution, preventing SQL injection and allowing plan reuse. Session-pinned features (temp tables, some SET options) interact badly with some pooler modes, validate driver + pooler behavior. Use bind types that match column types to avoid implicit cast surprises.