SQL
elements man database/sql Read as markdownWriting queries with the sql tag, the sync-style async transform, and
camelCase column mapping.
sql<T>(text: string, args?: unknown[]): SqlResult<T>
Write SQL as a template string and interpolate any runtime value with
${value}.
let user = sql<User>(`select * from users where id = ${id}`).first();
let active = sql<User>(`select * from users where active = ${true} and orgId = ${orgId}`);
The build extracts each ${value} into a positional parameter at compile time.
The runtime call becomes text + args and Postgres parameterization happens at
the protocol layer, so interpolations are always safe from injection.
sql() must be called from the server. If browser code calls it without going
through an @rpc, you get a compile error pointing at the call site.
The return value is a SqlResult<T>: an iterable wrapper over the rows. There
is no model layer, no class instances, and no lazy loading. The generic renames
the result type for the type checker and does not change runtime behavior.
let users = sql<User>(`select id, name, email from users`);
for (let u of users) { /* ... */ }
users.all().map(u => u.name);
The result exposes:
.first(): the first row, orundefined..last(): the last row, orundefined..all(): every row as a plain array..length/.size: the row count..empty():truewhen there are no rows..firstOrThrow(msg?)/.lastOrThrow(msg?)/.allOrThrow(msg?): same as the above, but throwsNotFoundError(a safe 404) if empty.
sql<User>(`select * from users where id = ${id}`).first();
sql<Event>(`select * from events order by createdAt desc`).last();
sql<User>(`select * from users`).all();
// throws NotFoundError to the client if the row is missing
let user = sql<User>(`select * from users where id = ${id}`).firstOrThrow();
Sync-Style and the Async Transform
sql() is written sync-style. At build time the compiler rewrites each call to
await sqlAsync() and propagates async up the call stack. Every function that
transitively calls sql() becomes async without you typing anything. The code
reads as a sequence of statements and runs async underneath.
When you need explicit promise control, import sqlAsync or txAsync directly
and use async/await as normal. This is the right tool for concurrent reads
with Promise.all, or for interleaving SQL with awaited third-party calls. The
build leaves your manual async/await alone.
import { sqlAsync } from "@elements/app";
let [users, posts] = await Promise.all([
sqlAsync<User>(`select * from users`),
sqlAsync<Post>(`select * from posts`),
]);
camelCase
By convention, write camelCase identifiers in your SQL. Elements converts to snake_case at the database boundary and back to camelCase on results, so the SQL reads like the TypeScript that surrounds it. You write one casing throughout the codebase.
let user = sql<User>(`
insert into users (firstName, createdAt) values (${name}, ${now}) returning *
`).firstOrThrow();
user.firstName;
user.createdAt;
// On the wire to Postgres:
// INSERT INTO users (first_name, created_at) VALUES ($1, $2) RETURNING *
The same convention applies in LiveTable config, in migrations, and anywhere
SQL meets TypeScript. Elements handles the boundary.
The one place this conversion does not apply is psql. elements db and its
-sql flag send text straight to Postgres, so there you write the snake_case
names stored in the database. See elements db → Shell below.