sql.params
import sql.params
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.params.*needsimport sql.params.
Rewriting a statement’s placeholders into whatever the active adapter expects.
Engines disagree about how a parameter is spelled. PostgreSQL wants
$1, SQLite accepts ?1, others want a bare ?. Without a translation
step, switching adapters would mean rewriting every statement in a
program, which is most of what makes changing database an ordeal.
So sql defines one spelling and translates. Statements are written
with ? for positional parameters or :name for named ones, and each
adapter’s driver declares which form it needs.
translate('select * from t where a = ? and b = ?', [1, 2], NUMBERED)
# { sql: 'select * from t where a = $1 and b = $2', values: [1, 2] }
The scanner understands enough SQL to know when a ? or a : is not a
placeholder: inside a string, a quoted identifier, a bracketed
identifier, a comment, or a PostgreSQL dollar-quoted body, and in the
:: cast operator. A statement that needs the literal character rather
than a placeholder writes ??, which becomes a single ?. That matters
for PostgreSQL, where ? is a JSON containment operator.
Nothing here escapes a value or builds SQL out of one. Values are bound by the engine, always.
Constants
QUESTION
sql.QUESTION = 'question'
A bare ? for every parameter, in order. MySQL and MariaDB.
NUMBERED
sql.NUMBERED = 'numbered'
$1, $2 and so on, numbered from one in binding order. PostgreSQL.
INDEXED
sql.INDEXED = 'indexed'
?1, ?2 and so on. SQLite, which accepts a bare ? too but is given
explicit indices so a repeated named parameter can be bound once rather
than once per mention.
STYLES
sql.params.STYLES = [...]
Every style a driver may declare.
Functions
placeholder()
sql.placeholder(style: string, position: number) -> string
The placeholder text for the parameter at position, counting from one.
Used by the statement builders in crud, which generate SQL and so need
to spell placeholders themselves rather than translate them.
Parameters
style(string) — One ofQUESTION,NUMBEREDorINDEXED.position(number) — The parameter’s 1 based binding position.
Returns string
Raises QueryError if style is not one this module defines.
tokenize()
sql.params.tokenize(sql: string) -> list[dict]
Splits sql into the pieces that matter, in order.
Each token is a dictionary with a kind:
'text'is SQL to copy through,textholding it.'opaque'is a run nothing inside counts in: a string, a quoted or bracketed identifier, a comment, or a dollar-quoted body. Also copied through, but never looked inside.'question'is a positional placeholder.'named'is a named one,nameholding the name without its colon.'separator'is a semicolon at statement level.
Everything in this module reads the same statement the same way because everything in this module goes through here.
Parameters
sql(string)
Returns list[dict]
scan()
sql.scan(sql: string) -> dict
What placeholders sql uses, without needing any values.
prepare() needs this: it translates a statement before it has anything
to bind, so it has to know what the statement asks for.
Parameters
sql(string)
Returns dict — { positional, names }, positional the count of
? and names the :name names in first-mention order.
split_statements()
sql.split_statements(sql: string) -> list[string]
Splits a script into its statements.
Only semicolons outside strings, identifiers and comments separate, so a semicolon inside a string literal or a trigger body does not split the statement it belongs to. Empty pieces, which a trailing semicolon leaves behind, are dropped.
Parameters
sql(string)
Returns list[string]
translate()
sql.translate(sql: string, params, style: string) -> dict
Rewrites sql into style and puts the values in binding order.
Accepts two forms, and only one of them per statement:
- Positional.
?in the statement,paramsa list. Values bind in the order they appear. - Named.
:namein the statement,paramsa dictionary. A name used more than once binds once underNUMBEREDandINDEXED, and once per mention underQUESTION, which has no way to refer back.
Mixing the two in one statement raises, as does supplying a list for a named statement or a dictionary for a positional one. Each of those is a mistake with a silent wrong answer available, so none of them is guessed at.
Parameters
sql(string) — The statement, written with?or:name.params(list|dict|nil) — The values to bind.nilmeans none.style(string) — The active driver’s placeholder style.
Returns dict — { sql, values }, where values is always a list
in binding order.
Raises QueryError if the two forms are mixed, if params is the
wrong shape for the form used, if a positional statement and its list
disagree about how many values there are, or if a named statement
mentions a name the dictionary does not hold.
2026, Richard Ore and Zuri contributors