# Reading a SQLite Database
A pattern can be handed a SQLite database the way it is handed any other input:
by reference. The input is typed `SqliteDb`, and the pattern reads it with
`db.query`. It never opens a file, never names a path, and never picks a source
— whoever wired the input chose those.
The handle's readable value is a small descriptor, not the data. Reading it as
a value tells a pattern nothing it can compute over; the rows come back only
from a query.
## Declaring the input
Type the input `SqliteDb`, imported from `commonfabric`:
```tsx
// Shown at module scope.
import { pattern, type SqliteDb } from "commonfabric";
interface Input {
orders: SqliteDb;
}
export default pattern(({ orders }) => ({
count: orders.query<{ n: number }>("SELECT count(*) AS n FROM orders"),
}));
```
`SqliteDb` is the whole declaration. A pattern that widens it — to `any`, or to
a hand-written object type with a `query` method on it — compiles and then
receives the handle's descriptor value rather than the handle, so `query` is
missing at run time. What makes the input arrive as a handle is the declared
type: `SqliteDb` is what the compiler lowers to the cell-shaped input the
runtime binds a database to.
## db.query
`db.query(sql, options?)` is a reactive read, not a promise. Never `await`
it. Call it in the pattern body and read `pending` / `error` / `result`
reactively; it re-runs when its inputs change, and results are memoized per
request.
```tsx
// Shown at module scope.
export default pattern<{ orders: SqliteDb }>(({ orders }) => {
const pending = orders.query<{ id: number; glaze: string; boxes: number }>(
"SELECT id, glaze, boxes FROM orders WHERE shipped = 0 ORDER BY id LIMIT 200",
);
return lift((rows?: Array<{ glaze: string; boxes: number }>) =>
(rows ?? []).map((row) => `${row.boxes} × ${row.glaze}`).join(", ")
)(pending.result);
});
```
The call returns an envelope, not the rows:
`{ pending, result?, error?, withheld? }`. `result` holds the rows once there
are any, so a field typed as the rows themselves — `PerSession` — cannot
take the call's value, and reading `.result` is what gets from one to the
other. `withheld` counts the rows a read-time clearance kept from this reader,
and is absent unless the query asked for one.
Render the failure, not just the wait. A pattern that branches only on
`pending` shows a loading view for as long as the query stays broken, because a
query that failed is settled — `pending` is `false` and `error` holds the
reason — and nothing further arrives to move it on. `error` reaches the pattern
for a statement the database refuses and for a handle that does not read back
as one, so a view that shows it is the difference between a page that says what
went wrong and a page that spins.
A read refused for labeling arrives the same way and is the case most easily
mistaken for an empty source: where a table declares a per-row label rule, a
query that does not project every column the rule reads is refused rather than
returned unlabeled, and `error` names the column and says to select it.
`result` is then absent, not empty — so a view that renders "none found" over
it states a fact about the data that nobody established. Branch on `error`
before branching on emptiness.
The `` type argument names the columns the statement projects, and is what
turns a result into something typed. Without it the rows come back as
`Record`, and a `_cf_link` column comes back as a raw link
string rather than a live cell.
The tables are declared on the database rather than on the pattern, so a
pattern reading an input database does not restate them. What it needs to know
is which tables and columns are there — the shape of the contract it is
querying against — and that is a property of the handle it was given.
Where a pattern also writes to the database, pass `{ reactOn: db }` so the read
re-runs after a committed write. An input a pattern only reads has nothing to
react to.
## A param is a value
A bind param is resolved as the statement is issued, and `undefined` is refused
rather than bound: the whole read fails with "sqlite: param is undefined",
`error` carries that text, and every field computed from the result is empty.
`null` is what binds SQL NULL.
An input's declared default is applied when the input is READ, so an input
handed to `params` as itself rather than as a value read out of it arrives
undefaulted. That costs nothing until someone composes the pattern: a caller
forwards an optional input of its own, which reads `undefined` while nobody has
supplied one, and the read the pattern's own default was written to cover is
the read that fails. Leaving the argument key out is the only shape the default
covers on its own.
```tsx
// Shown at module scope.
import { computed, Default, pattern, type SqliteDb } from "commonfabric";
export default pattern<{ ledger: SqliteDb; month?: string | Default<""> }>(
({ ledger, month }) => {
// Read out of `month` rather than `month` itself, so a caller forwarding a
// month nobody supplied gets the empty string the statement resolves.
const monthParam = computed(() => month);
return ledger.query<{ id: number }>(
"SELECT id FROM rows_plaid_transaction WHERE substr(date, 1, 7) = " +
"COALESCE(NULLIF(?, ''), strftime('%Y-%m', 'now')) LIMIT 200",
{ params: [monthParam] },
);
},
);
```
## One statement, one database
A query is a single read-only `SELECT` (a read-only CTE counts). Multiple
statements, DML, DDL, `PRAGMA`, `ATTACH` and `DETACH` are refused, and so is a
schema-qualified table name. The consequence worth planning around: one
statement reads the tables of the one database it was issued on, and no
statement can join across two handles. Two databases means two queries and a
join expressed in the pattern.
## Bound the rows
Every result row is written into the space as a document of its own — which
is what gives a per-row label somewhere to sit — so the row count of a
statement is a durable cost of the space rather than the cost of one render. A
query that returns a million distinct rows writes a million documents, and they
stay written after the view that asked for them is gone. The query writes a
row document once and never rewrites it. It is keyed on the row's content and,
for a row of a labeled database, on its label, beside a per-space secret that
keeps the id from saying anything about the row. Equal rows share one
document, a row the result held before takes its old document back, and a
re-run whose rows are unchanged writes no row documents. A row whose data or
label changed is another document, and the one it had stays in the space. A
reference a pattern keeps to a row therefore does not change when the query
runs again: read the query's `result` for the current rows. The query is not
the only writer that can reach a row document, though: other code holding a
reference to one can write to it. Three things re-key every row of a labeled
database at once and write it again: changing which database the query
reads, changing the query's projection, and re-declaring the handle's
`tables`. A row projecting a column name a Fabric
record reserves (`constructor`, `__proto__`) crosses the wire as a list of
entries and is stored the same way.
A statement therefore bounds its rows, and a filter is not a bound. A `WHERE`
clause narrows the candidates and says nothing about how many survive it: a
month of a large store is still most of a large store. `LIMIT` is what states
the ceiling, and a tight filter under it is what makes the rows the ceiling
admits the interesting ones.
A few hundred rows is a sensible ceiling for a view. It is more than a reader
takes in at once, and it keeps what one query leaves behind in the space
proportionate to what the view displays. A view that needs more of the store
than that pages through it — a bound the reader moves. Paging bounds what one
query writes rather than what the space accumulates: every page fetched
materializes its rows, and nothing reclaims the rows of a page the reader has
left. Returning to an earlier page issues a fresh query, and its rows land on
the documents they had before. The durable cost is
the number of distinct row documents the pages materialize, which grows with
every page whose rows the space has not held.
Project the columns the view reads and no others: a row document carries every
column the statement selected, so a wider projection is paid on every row.
`LIMIT` bounds the rows a query returns, and therefore the documents it writes.
What the database spends finding those rows is a separate question, and the
query plan answers it: where an index covers the ordering, `ORDER BY … LIMIT n`
reads in order and stops at n; where none does, the database scans the
candidates and sorts them through a temporary B-tree before the first row comes
back. So `LIMIT` alone bounds the writing and not the reading, and a `WHERE`
clause narrow enough to keep the candidate set small is what bounds the work of
an ordering the database cannot walk.
Where a view needs a number rather than the rows, ask SQL for the number.
`count(*)`, `sum()` and a `GROUP BY` return one row per group, and one row is
one document; the same answer reached by returning the rows and counting them in
the pattern writes a document per row on the way. An aggregate over a table that
derives its labels from row data is the case to check before relying on one: it
is admitted where the contributing rows have a reader in common, and refused
where they do not, because no principal is then guaranteed to read everything
the aggregate summed. The rule is in
[06](../../specs/sqlite-builtin/06-cfc.md#read--re-derive-per-row-attach-ceiling-dbquery).
```tsx
// Shown at module scope.
export const newestOrders = (orders: SqliteDb, since: number) =>
orders.query<{ id: number; glaze: string }>(
"SELECT id, glaze FROM orders WHERE placed_at >= ? " +
"ORDER BY placed_at DESC LIMIT 200",
{ params: [since] },
);
```
## A connector store's tombstones, and a query that returns no rows
A connector-backed database carries the conventions of the connector that fills
it, and a pattern reading one is held to them. Two conventions for a deleted row
are in use. One marks a live row with a NULL timestamp, so `deleted_at IS NULL`
selects the rows still standing. The other keeps every text column never-NULL
and puts the state in an integer flag beside it: `deleted = 0` is a live row,
and `deleted_at` holds `''` until a tombstone stamps it. Where a table declares
a `deleted` flag, filter on the flag — a NULL test against the text column of
such a table selects nothing at all, because no row in it ever holds NULL
there.
The table declaration is what to read before writing the predicate. It names the
columns and the label each one carries; which value in a column means a deleted
row is the connector's convention, and the declaration does not state it. So a
predicate carried over from another database is a guess about the one being
queried: every column it names may be there while the value it tests for means
something else. Two databases in one task can disagree about this, and each is
right about itself.
An empty result is a value rather than a failure. A statement matching nothing
settles the way one matching everything does — `pending` false, `error` absent,
`result` an empty list — and every field computed from it is empty in turn, so
the view renders its empty state and nothing reports a problem. The run
succeeded; the emptiness is data.
That is what makes an empty source worth one check before it is believed. Count
the table without the predicate you are least sure of — the one testing a
sentinel — and compare the two: rows under the count and none under the
predicate put the disagreement in the predicate rather than in the store. Where
the emptiness is then reported onward, name the predicate that produced it, so a
reader of the result learns which condition emptied the source rather than that
the source was empty.
```tsx
// Shown at module scope.
export const liveOrders = (orders: SqliteDb) => ({
rows: orders.query<{ id: number; glaze: string }>(
"SELECT id, glaze FROM orders WHERE deleted = 0 " +
"ORDER BY id DESC LIMIT 200",
),
total: orders.query<{ n: number }>("SELECT count(*) AS n FROM orders"),
});
```
## Session-scoped results
Where the runtime carries a read ceiling — a lens on what this particular run
may observe — a **session-scoped** result filters its rows under that ceiling.
A space- or user-shared result materializes independently of runtime ceilings.
Its array shape and rows retain their confidentiality labels, and a read
outside the observing runtime's ceiling is withheld. A shared result containing
private rows can therefore withhold the whole array, including its length.
That membership label retains its confidentiality across refreshes, even after
rows are removed or relabeled. An addressed row's payload keeps its own label
without inheriting the array's membership label.
Declare the scope on the query:
```tsx
// Shown at module scope.
export const recentOrders = (orders: SqliteDb) =>
orders.query<{ id: number; glaze: string }>(
"SELECT id, glaze FROM orders ORDER BY id DESC LIMIT 20",
{ scope: "session" },
);
```
`PerSession<>` on the result field and `.asScope("session")` on the query do
the same thing, and a session-scoped database makes its queries session-scoped
without a declaration. Use a session-scoped result when each session needs its
own filtered row set.
## Labeled columns
A column may declare an `ifc` label on the database's table schema, and that
label rides into the result: a row read out of a labeled column carries what
the column requires, and code downstream of the read is held to it. Two
options bound a read against such a column — `maxConfidentiality` for a ceiling
the result may not exceed, and `onExceed` to choose between failing the query
and dropping the rows that exceed it.
Where the label lands decides where to look for it. Each result row splits into
its own entity doc and the column's label sits on that doc, at the column's own
path. The query document labels `result` membership with the join of the
source rows' labels and the label of the query's own statement and parameters,
and labels the `withheld` count with that same join. A shared result joins
every source row's label, including rows skipped by the query contract; a
session-scoped result joins the labels of the rows it holds. Which row sits at
a position of `result` carries the label of the query's statement and
parameters and that row's label. A
reader outside the join therefore cannot observe the array's membership,
length, or withheld count, and code that reads any of them carries the join
into what it writes: a row count of a query over labeled columns carries the
columns' labels, and so does anything computed by mapping over `result`,
which reads its membership. A row read through `result` by its position
carries the label of the query's statement and parameters and that row's own
labels, and not the other rows'. `cf cell get-label /result//
` follows the links
the path crosses and reports the
column's label from the row's own doc. Inside a pattern nothing has to be asked
for: a consumer inherits the label from the dereferences its read traverses.
## The rest of the API
[`docs/specs/sqlite-builtin/`](../../specs/sqlite-builtin/README.md) is the full
specification: the handle type and the method surface in
[01](../../specs/sqlite-builtin/01-api.md), links in columns in
[02](../../specs/sqlite-builtin/02-cf-link-encoding.md), where a database comes
from in [03](../../specs/sqlite-builtin/03-database-sources.md), the statement
guard and transactions in
[04](../../specs/sqlite-builtin/04-server-execution-and-transactions.md),
reactivity in [05](../../specs/sqlite-builtin/05-reactivity.md), and the label
rules in [06](../../specs/sqlite-builtin/06-cfc.md).