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.