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 manual revalidate() — 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.