Queries & caching
How to read and write through the ambient db — the query surface, plain-SQL migrations, and the auto-batch / auto-invalidate cache.
Reading
Reach the database with the ambient db (the data model
explains why it's ambient and off ctx) and query it:
import { db } from "@junejs/db";
export const loader = async () => ({
users: await db.query("select id, name from users order by id"),
});
The dev default is a plain file at .june/dev.sqlite that survives the dev
server's reload-on-save restarts — and the sqlite3 already on your machine
opens it directly:
sqlite3 .june/dev.sqlite '.tables'
Migrations — the SQL you read is the SQL that runs
A connected database does not invent tables. Schema lives in versioned SQL you can read and diff:
-- db/migrations/0001_init.sql
create table users (id integer primary key, name text not null);
june dev applies pending migrations on startup — safe, additive ones
automatically (create table, add column, a new index). A destructive
change (drop, a narrowing alter) halts with the safe prefix already applied
and asks first; you run it deliberately with june db migrate --allow-destructive.
Add the next change as 0002_*.sql — never edit an applied file. The same
ordered ledger runs at deploy against D1, so dev and production converge on one
schema.
Caching — auto-tag, auto-invalidate, auto-batch
Reads and writes flow through a small public trace contract, which is all the framework needs to make caching a consequence of the write, not a chore:
import { db } from "@junejs/db";
const listUsers = cache(() => db.table("users").all()); // tagged [table:users]
await invokeAction("createUser", { name: "Ada" }); // writes users →
await listUsers(); // cache MISS — fresh
- auto-tag — every
cache()entry is tagged by the tables it read. - auto-invalidate — a write to a table drops exactly those entries. No
revalidatePath, no tags to remember, no manualrevalidate()— ever. - auto-batch — N component reads of the same query on one render collapse to a single round trip (measured: 8.8× fewer D1 queries vs naive per-component reads — see Benchmarks).
Stale-cache bugs are the classic failure of hand-wired revalidate() calls, and
agents forget them exactly as often as humans do. Making invalidation fall out
of the write removes the whole class.
kv and blob
The other resources follow the same ambient shape — memoryKv() / localBlob()
in dev, redisKv() / r2() on deploy — reached as ambient kv and blob:
import { kv, blob } from "@junejs/db";
await kv.set("greeting", "hi");
await blob.put("avatar.png", bytes);
Status: Changing — see Stability.