Guides
Compose SQL templates
Use trusted SQL syntax with bound runtime values while retaining the Qubu metadata carried by interpolated expressions, fragments, and queries.
Bind every runtime value
The sql tag treats template text as SQL syntax and every ordinary
substitution as a parameter. This includes strings, numbers, objects, arrays,
and null:
import { integer, render, sql, table, text } from "qubu"
const users = table("users", { name: text() })
const posts = table("posts", { id: integer() })
const search = "Ada%"
const predicate = sql`${users.name} LIKE ${search}`
render(predicate)
// {
// text: '"users"."name" LIKE ?',
// parameters: ['Ada%'],
// }
${users.name} is a Qubu expression, so the tag renders its quoted column
reference. ${search} is an ordinary value, so it cannot become SQL text.
Compose expressions, templates, and queries
Fragment substitutions use the same rendering context as the enclosing statement. Parameters keep one placeholder sequence across nested templates and queries:
import { eq, from, render, select, sql, where } from "qubu"
import { postgresDialect } from "qubu/postgres"
const selectedNames = select({ displayName: users.name }, from(users), where(eq(users.name, "Ada")))
const exists = sql`EXISTS (${selectedNames}) AND ${users.name} <> ${"root"}`
render(exists, postgresDialect())
// {
// text: 'EXISTS (SELECT "users"."name" AS "display_name" FROM "users" WHERE ("users"."name" = $1)) AND "users"."name" <> $2',
// parameters: ['Ada', 'root'],
// }
The template owns punctuation such as the parentheses around the query. Qubu renders a query substitution with SQL-facing projection names, as it does for CTEs and scalar subqueries.
Declare a result domain
An unannotated template has application output unknown and SQL domain
SqlUnknown. State both facts when the expression feeds typed operations or a
named projection:
import { from, select, sql } from "qubu"
import type { SqlText } from "qubu"
const normalizedName = sql.type<string, SqlText>()`LOWER(${users.name})`
const query = select({ name: normalizedName }, from(users))
// typeof query.row is { name: string }
sql.type<Output, SqlType>() changes only TypeScript metadata. It does not
parse the SQL or validate the declared types against the database. The tag
still binds ordinary substitutions.
Interpolated fragments contribute their source requirements and
nullability. For example, a sql.type<string, SqlText>() template containing a
column from the right side of a leftJoin() produces string | null in the
selected row.
Keep identifiers and dynamic syntax explicit
Use the substitution kind that matches the value:
| Input | Rendering behavior |
|---|---|
| Fixed template text | Trusted SQL syntax, appended unchanged |
| An ordinary substitution | Bound parameter |
| A Qubu expression, query, or fragment | Rendered through the current dialect and placeholder sequence |
identifier(name) |
One dialect-quoted identifier |
qualifiedIdentifier(schema, table) |
Several dialect-quoted identifier parts |
unsafeExpression(text) |
Unchecked SQL text |
Do not use a dotted string as an identifier. Pass each part to
qualifiedIdentifier(). Keep unsafeExpression() for application-controlled
syntax that cannot use a fixed template segment:
import { sql } from "qubu"
import { identifier, unsafeExpression } from "qubu/core"
const sortColumn = "display_name"
const direction = "DESC" as const
const ordering = sql`ORDER BY ${identifier(sortColumn)} ${unsafeExpression(direction)}`
Validate any dynamic syntax against an application-owned allowlist before it
reaches unsafeExpression().
Preserve metadata through interpolated fragments
The tag inherits source dependencies, conservative outer-join nullability, grouping facts, aggregate and window state, subquery state, and dialect capability requirements from Qubu fragment substitutions. It does not infer those facts from unchecked template text.
Use a built-in expression as the substitution when its semantics matter:
import { count, sql } from "qubu"
import type { SqlInteger } from "qubu"
const postCount = sql.type<number, SqlInteger>()`${count(posts.id)}`
This wrapper retains the aggregate dependency recorded by count(). Writing
the aggregate name in template text would not record that fact:
const untrackedPostCount = sql.type<number, SqlInteger>()`COUNT(${posts.id})`
This renders valid SQL, but Qubu sees an ordinary column dependency because it
does not parse COUNT from the template text.
Declare a capability when the template text itself uses dialect-specific syntax:
import { sql } from "qubu"
import { withDialectCapability } from "qubu/core"
import type { SqlBoolean } from "qubu"
const postgresMatch = withDialectCapability(
sql.type<boolean, SqlBoolean>()`${users.name} ILIKE ${search}`,
"ilike",
)
Rendering postgresMatch now requires a dialect that advertises ilike.
Use Fragments and metadata for the inherited facts, and SQL semantic types for result-domain compatibility.