> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Virtual queries

> Declare typed analytical queries and browse retained dataset snapshots

Declare an analytical query on the server, then read it through `useVirtualQuery`.
Arguments and row types flow through the generated API. The hook retains one query
identity, follows its execution, and releases it when its arguments change or the
component unmounts. Refresh explicitly starts a new query.

## Choose the answer you need

| Result | Meaning | Paging |
| - | - | - |
| `complete` | One complete bounded answer, including joins, grouping and sorting | Pages come from the same saved result; `rowCount` is exact |
| `browse` | Rows from one retained dataset snapshot, optionally filtered and projected | Each page is read from that snapshot; `rowCount` is `null` |

A complete answer that exceeds its bounds fails instead of returning a partial
answer. Browse does not compute a population-wide total or sort. New dataset
publications do not change either result. Current read and disclosure rules still
apply to every read.

## Declare the input

An installed dataset can be exposed as a virtual table. Import its existing
`defineDataset` declaration; deployment administration owns catalog binding and
credentials. The dataset must have a published version and managed retention.

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineSchema, defineVirtualTable } from "bijection/server";
import { internal } from "./_generated/api";
import { sales } from "./datasets";

export default defineSchema({
  warehouseSales: defineVirtualTable(sales).access({
    read: internal.access.readSales,
    disclose: internal.access.discloseSales,
    reads: [],
  }),
});
```

The read rule authorizes the selected input, including fields used by predicates.
The disclosure rule separately authorizes retained results and query computation.
See [disclosure rules](/access/disclosure). A virtual table has no local documents
or indexes and cannot be written through `ctx.db`.

Dataset queries currently support present scalar string, integer, finite float,
boolean and byte fields, including nullable fields. Project unsupported optional or nested
fields out of the query. Complete queries can combine dataset inputs with bounded
local inputs; combining dataset inputs with live SQL inputs is refused.

## Declare the query

Import `defineVirtualQuery` from the generated server module so the result type is
resolved against your schema. The client supplies arguments; the declaration owns
the expression and its bounds.

```ts bijection/analytics.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { q } from "bijection/server";
import { v } from "bijection/values";
import { defineVirtualQuery } from "./_generated/server";

export const salesByRegion = defineVirtualQuery({
  args: { region: v.string() },
  result: "browse",
  query: ({ region }) =>
    q.table("warehouseSales")
      .filter(q.eq(q.field("region"), region))
      .select({ quantity: q.field("quantity") }),
});
```

Use `result: "complete"` for a complete answer. `maxRows` and `maxBytes` bound that
answer; exceeding either refuses the result. Queries expire rather than silently
starting again.

## Read it in React

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { useVirtualQuery } from "bijection/react";
import { api } from "../bijection/_generated/api";

export function Sales({ region }: { region: string }) {
  const result = useVirtualQuery(api.analytics.salesByRegion, { region }, {
    pageSize: 50,
  });
  if (result.state === "preparing") return <p>Loading sales…</p>;
  if (result.state === "failed") return <p>{result.error.message}</p>;
  return <section>
    <button onClick={result.refresh}>Refresh</button>
    <ul>{result.items.map((row, index) =>
      <li key={result.references?.[index]?.key ?? index}>
        {row.quantity.toString()}
      </li>
    )}</ul>
    <button disabled={!result.hasMore || result.isLoadingMore}
      onClick={result.loadMore}>Next page</button>
  </section>;
}
```

A browse page contains at most 1,000 rows and 4 MiB of row values. `pageSize` can request a
smaller page. Browse replaces the displayed page, keeping client memory bounded. A filtered page
can be empty while `hasMore` is true. Complete-result paging accumulates the bounded
answer. Pass `"skip"` instead of arguments to suspend the hook. Integers remain
`bigint` throughout the flow.

## Open a snapshot row

Browse pages return one `SnapshotRowReference` per row. `result.readRow(reference)`
reopens that exact row under current authorization; the browser query client's
corresponding method is `execution.row(reference)`. A reference becomes unavailable
when its query is released or expires. It grants no authority by itself.

A native table export may also supply `reference.currentObject`. This is a separate
reference to the current document. Read it through the ordinary object API, which
checks current access and can return a changed value or a deleted object. Never
pass the snapshot reference itself to a mutation or treat its key as a current ID.
Explore presents these as separate snapshot details and an **Open current object**
link when the object is exposed by the application.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.