Run a query written as a tagged template, and return its rows.
Every interpolation becomes a bind parameter, so an interpolated value is never SQL text and cannot change the statement's shape:
const rows = await ctx.postgres.query`select * from notes where id = ${id}`
// -> select * from notes where id = $1 with values [id]
Tagged templates cannot carry type arguments, so annotate the binding
rather than writing query<NoteRow>:
const rows: NoteRow[] = await ctx.postgres.query`select id, body from notes`
pg returns date, timestamp, and timestamptz columns as Date
objects. Declare those fields as Date in the row type, or cast in SQL
(day::text as day) when the value feeds a PostgREST filter or a JSON
body.
Identifiers — table, column, order by direction — cannot be bind
parameters in Postgres. Check them against a set you control and quote
them with ident, then use PostgresApi.queryRaw.
Passing a plain string throws, naming queryRaw. That is deliberate: the
two calls differ only in their brackets, so a silent reinterpretation
would be very hard to spot.
Run a query from SQL text you supply, and return its rows.
Safe when every caller-supplied value travels in params — that is
exactly what PostgresApi.query compiles to. Reach for this when
the text cannot be a literal: a query builder or codegen emitting
{ sql, parameters }, or SQL that has to interpolate an identifier
(quote it with ident first).
const rows = await ctx.postgres.queryRaw(
'select * from notes where id = $1',
[id],
)
Unlike query, this cannot stop you concatenating a value into text.
The name is the warning, and it greps.
Optionalparams: unknown[]
The shape of
ctx.postgresandctx.postgresAdmin.Both halves expose the same surface — they differ in what runs around the query, not in how you call it.
withPostgresClientwraps every query in a transaction that injects the caller's claims and drops to their role;withPostgresAdminClientruns it as-is, as the connection-string role.