Give this to your AI agent
Larva is built for apps where an agent writes the SQL. This page holds the prompt that teaches an agent the dialect, the guardrails, and the performance rules — paste it into your agent’s instructions (CLAUDE.md, AGENTS.md, .cursorrules, a system prompt).
Agents can also fetch it directly: it’s served raw at
larvadb.dev/llms.txt (and at /llms.txt on any deployed test
lab) — one URL instead of a paste.
The prompt
# Working with LarvaDB (@larva-db/core)
This project uses **Larva**, a SQL database that lives inside a Vercel Blob /
S3 / R2 object store. You query it with real SQL through tagged templates.
## Querying
- Query with `db.sql` tagged templates. ALWAYS interpolate values with
`${...}` — they are parameterized automatically. Never build SQL by string
concatenation.
- The schema is defined in code with `defineSchema` — use those exact table
and column names.
- Timestamps are ISO 8601 text. Compare them directly as strings:
`WHERE createdAt >= ${"2026-07-01"}`.
## Supported SQL
- `SELECT` (with `DISTINCT`) over expressions: arithmetic, `||` concatenation,
`CASE WHEN`, `CAST(x AS text/integer/real/boolean)`.
- Scalar functions: `UPPER`, `LOWER`, `LENGTH`, `TRIM`, `ROUND`, `ABS`,
`COALESCE`, `NULLIF`, `IFNULL`, `REPLACE`, `CEIL`, `FLOOR`, `MOD`, `SUBSTR`.
- Dates: `NOW()`, `CURRENT_TIMESTAMP`, `DATE(x)`, `STRFTIME('%Y-%m', x)`.
- JSON over text columns: `JSON_EXTRACT(col, '$.a.b[0]')` and `col ->> 'key'`.
- `WHERE` with `=`, `!=`, `<`, `>`, `<=`, `>=`, `AND`, `OR`, `NOT`, `IN`,
`BETWEEN`, `LIKE`, `IS NULL`.
- `ORDER BY` (columns or select aliases), `LIMIT` / `OFFSET`.
- `GROUP BY` over expressions or aliases (e.g. `GROUP BY DATE(createdAt)`)
with `COUNT` / `SUM` / `AVG` / `MIN` / `MAX` / `GROUP_CONCAT(x, sep)`,
including `COUNT(DISTINCT col)`, plus `HAVING`.
- `INNER JOIN` and `LEFT JOIN` on equality — any number of tables, including
self-joins (alias each occurrence: `FROM staff e JOIN staff m ON
e.managerId = m.id`).
- Uncorrelated subqueries: `WHERE id IN (SELECT ...)`, `NOT IN (SELECT ...)`,
and scalar comparisons like `WHERE total > (SELECT AVG(total) FROM orders)`.
The subquery must NOT reference the outer query's tables (no correlation) —
use a JOIN for that. NULLs in the subquery result are ignored, so
`NOT IN (SELECT ...)` behaves the way you intend even when the inner column
has NULLs (unlike standard SQL's NULL trap).
- `INSERT ... RETURNING`, multi-row, with upsert:
`ON CONFLICT (col) DO NOTHING` or `DO UPDATE SET col = excluded.col`.
The conflict target must be the primary key, a UNIQUE column, or the exact
columns of a composite unique declared in the schema
(`ON CONFLICT (userId, feature) DO UPDATE ...`).
- `UPDATE ... WHERE`, `DELETE ... WHERE`, `CREATE TABLE`, `DROP TABLE`.
- `ALTER TABLE t ADD COLUMN name type` — plain nullable columns only.
Existing rows read the new column as NULL; backfill with UPDATE if needed.
- `CREATE INDEX ON table (column)` / `DROP INDEX ON table (column)` — one
index per column, addressed by column (index names are ignored). Indexes
make =/IN/range filters on that column prune storage reads; they are
performance-only and never affect results.
## Schema features to know
- A `t.uuid()` column is an auto-assigned ID: OMIT it on INSERT and read the
generated UUID back with `RETURNING`. Prefer it for row identity — the
writer invents the value, so nothing ever contends. Supplying your own
value is allowed and respected.
- A `t.sequence()` column is an auto-assigned integer: OMIT it on INSERT and
read the assigned value back with `RETURNING`. Never generate the number
yourself. Numbers are unique across concurrent writers but gappy (like a
Postgres sequence). Use it when humans need small numbers (invoice #42);
otherwise prefer `t.uuid()`.
- Composite unique constraints come from `defineSchema`'s second argument:
`defineSchema(spec, { uniques: { orders: [["customerId", "sku"]] } })`.
- `.index()` on a column (e.g. `t.text().index()`) maintains a secondary
index so filters on it prune storage reads. Use it for columns you filter
on often (foreign keys, emails, statuses); the primary key and the
`.partitionBy()` column never need it.
## NOT supported — do not emit
Correlated subqueries, subqueries in `FROM` (derived tables), window
functions, `UNION`, `RIGHT`/`FULL`/`CROSS` joins, `DROP COLUMN`/`RENAME`,
views, triggers, nested aggregates. If a query needs these, fetch the data
and compute in application code instead — tables here are small. Rejections
name the feature and say what to do instead; read the error message and
follow it.
## Guardrails
- `UPDATE` or `DELETE` without a `WHERE` clause is rejected unless you pass
`{ allowFullTable: true }`. Only pass it when a full-table write is truly
intended.
- Use `db.transaction(async (tx) => { ... })` for multi-statement changes —
they commit atomically or not at all.
- Writes can throw `ConflictError` under heavy concurrency after retries.
Surface it; never swallow it.
- If something goes wrong, `db.asOf(pastDate)` reads an old version and
`db.rollbackTo(version)` restores it — destructive mistakes are reversible.
## Performance rules of thumb
- Filters on the primary key, the one `.partitionBy()` column, or any
`.index()`ed column prune storage reads aggressively. Filter on the RAW
column: `createdAt >= ${"2026-07-01"}` prunes; `DATE(createdAt) >= ...`
scans (still correct, just slower).
- Everything else scans the table — fine at tens of thousands of rows.
- Write throughput is roughly one commit per second across all writers;
batch related statements into one transaction instead of many commits.
## Getting data out
- `db.export({ format: "postgres" })` → one `.sql` file; load with
`psql $DATABASE_URL < export.sql`.
- `db.export({ format: "sqlite" })` → a genuine SQLite `.db` file.
- `db.export({ format: "json" })` / `{ format: "csv" }` for everything else.
- From a shell: `npx larva sql "..."`, `npx larva export --format postgres` —
same database, same dialect, same errors (needs BLOB_READ_WRITE_TOKEN).
What it teaches, in five bullets
- Always interpolate with
${…}(parameterized automatically) — never concatenate SQL. - The supported dialect, and what to do instead for everything outside it.
UPDATE/DELETEwithoutWHEREneeds{ allowFullTable: true }; multi-statement changes go indb.transaction.- Filter on raw pk/partition columns for pruning (
createdAt >= '…', notDATE(createdAt) >= '…'). - Surface
ConflictError, never swallow it — anddb.rollbackTo()undoes mistakes.
Why errors look the way they do
Every rejection is machine-readable and names its alternative, because agents self-correct from specific messages:
UNSUPPORTED_FEATURE: window functions are not supported in Larva v1;
compute windows in application code — tables at this scale fit in memoryThe canonical source is docs/larva-for-agents.md in the repo — this page and /llms.txt render the same file, so none of the three can drift.
Last updated on