Querying with SQL
sql from platform:stdlib is the single entry point for reading and writing
data. It talks to real PostgreSQL — no query builder, no ORM dialect.
import { sql, tableRef } from "platform:stdlib";import { Issue } from "platform:entities";
const i = tableRef(Issue);
const { rows } = await sql.query<Issue>(` SELECT ${i.issue_id}, ${i.title} FROM ${i} WHERE ${i.status} = 'new'`);tableRef
Section titled “tableRef”tableRef(Entity) returns a proxy that renders as the table name, and whose
properties render as fully-qualified column references.
const i = tableRef(Issue);
`${i}` // the table`${i.title}` // the title column, table-qualifiedThe value is what makes the SQL schema-checked: i.titel is a TypeScript error,
not a runtime failure in production. Rename a field and every query referencing
it stops compiling.
Interpolating values
Section titled “Interpolating values”Never interpolate user input directly into the template string. Use bound parameters, passed as the second argument:
const { rows } = await sql.query<Issue>( `SELECT * FROM ${i} WHERE ${i.status} = $1`, [status],);For an IN list, sql.delimit escapes each value and joins with commas:
const { rows } = await sql.query<Issue>(` SELECT * FROM ${i} WHERE ${i.status} IN (${sql.delimit(["new", "assigned"])})`);Writing data
Section titled “Writing data”DML methods take arrays of entity instances. Each returns a DmlResult[]
parallel to the input.
| Method | Behavior |
|---|---|
sql.insert(records) | Insert; returns per-record results |
sql.update(records) | Update by record_id |
sql.upsert(records) | Insert or update on primary key |
sql.delete(recordsOrIds) | Delete by record or by id string |
const issue = new Issue();issue.title = "Disk nearly full";issue.component_id = componentId;
await sql.insert([issue]);Passing an empty array is a no-op that returns [] without a round trip, so you
do not need to guard the call.
Failing loudly
Section titled “Failing loudly”The plain methods report failure in their results rather than throwing — you
must inspect them. Each has an OrThrow variant that raises DmlBatchError
instead:
await sql.insertOrThrow([issue]);await sql.updateOrThrow(records);await sql.upsertOrThrow(records);await sql.deleteOrThrow(ids);Prefer OrThrow unless you intend to handle partial failure. To check manually:
import { assertDmlSuccess } from "platform:stdlib";
const results = await sql.insert(records);assertDmlSuccess(results); // throws DmlBatchError if any failedSystem mode
Section titled “System mode”sql.systemMode bypasses PostgreSQL row-level security. Everything else is
identical — same methods, same signatures.
const all = await sql.systemMode.query<Issue>(`SELECT * FROM ${i}`);await sql.systemMode.upsertOrThrow(records);Use it when code must act on records the triggering user cannot see: a nightly sweep, a cross-tenant rollup, an escalation that touches another team’s rows.
Other helpers
Section titled “Other helpers”await sql.validate(query); // parse and check without executingawait sql.schema(); // live table and column metadatasql is also callable directly as shorthand for sql.query:
const { rows } = await sql(`SELECT 1`);Limits
Section titled “Limits”query takes an options object for per-call caps:
await sql.query<Issue>(text, [], { rowLimit: 500, timeout: 5000 });Defaults come from server configuration — 1,000 rows and a 30-second timeout, with a 10,000-row hard ceiling.