Parameters¶
Rules of thumb:
- IN-lists. Write
= ANY({seq})withseqa list. - Literal braces. JSON and array literals in SQL are written
{{and}}. - No interpolation inside a string literal or comment.
LIKE '%{q}%'puts the text$1inside the literal. Bind the whole pattern instead:LIKE {pat}. The checker reports this asparam-count-mismatch, and at runtime the parameter count disagrees and raisesInterfaceError. - Cast parameters whose type the server can't infer. For example
{x}::int IS NULL. Without a cast, Postgres fails with 42P18 or 42P08. - Don't write
$nby hand. It causes the same count mismatch. - Reusing an expression (
{a}twice) creates two parameters.
Nullability overrides¶
The checker infers column nullability from the catalog and the query plan. When it can't prove a column is NOT NULL, it
treats it as nullable, and your annotation must say | None. Narrow it with a quoted alias:
AS "col!"forces NOT NULL.AS "col?"forces nullable.
The runtime ignores the suffix for tuples and scalars (rows are positional), so a NULL in a ! column still raises
RowShapeError. For a named row type the suffix is stripped from the name the column is matched under. Postgres
truncates identifiers at 63 bytes, which would drop the suffix; the checker reports that as invalid-override.
import asyncio
from rowx.postgres import connect
from rowx import query
async def main() -> None:
async with await connect(DSN) as conn:
n = await query[int](t'SELECT count(*) AS "n!" FROM users').one(conn)
assert n == 3
email = await query[str | None](t'SELECT email AS "email?" FROM users WHERE name = {"carol"}').one(conn)
assert email is None or isinstance(email, str)
asyncio.run(main())