> ## 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.

# Reading Views

> Read views from queries and mutations with the ordinary database API

You read a view exactly like a table: with `ctx.db` inside a
[query](/functions/query-functions) or [mutation](/functions/mutation-functions).
There's no separate client or query language.

```ts bijection/customers.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { query } from "./_generated/server";

export const listActive = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("active_customers").take(100);
  },
});
```

Clients call `listActive` like any other query, and receive new results when
the customers behind the view change.

## Reading a single object

Pass the view's name and an ID from its [key table](/views/defining-views#keys)
to `db.get`:

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

export const getActive = query({
  args: { customer_id: v.id("customers") },
  handler: async (ctx, args) => {
    return await ctx.db.get("active_customers", args.customer_id);
  },
});
```

The result is the view's row for that customer, or `null` when the customer
isn't a member of the view, for example because it's archived. The row's `_id`
and `_creationTime` are those of the `customers` document.

A few details follow from views sharing their key table's IDs:

* Always name the view. The one-argument form `ctx.db.get(id)` reads the table
  the ID belongs to, which is `customers`, not the view.
* An ID of another table is a type error: `ctx.db.get("active_customers", id)`
  requires an `Id<"customers">`.
* `ctx.db.normalizeId("active_customers", value)` checks that `value` is a valid
  ID of the key table. It doesn't check that the object is currently in the
  view; use `db.get` for that.

## Querying a view

`ctx.db.query` works on views with the usual methods for
[filtering](/database/reading-data/filters), [ordering](/database/reading-data/reading-data#ordering)
and retrieving results:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const emea = await ctx.db
  .query("active_customers")
  .filter((q) => q.eq(q.field("region"), "emea"))
  .collect();
```

Views also support [pagination](/database/pagination), including
`usePaginatedQuery` on the client:

```ts bijection/customers.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { query } from "./_generated/server";
import { paginationOptsValidator } from "bijection/server";

export const pageSummaries = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("customer_summaries")
      .paginate(args.paginationOpts);
  },
});
```

Results are typed from the view's expression. `Doc<"customer_summaries">` from
`./_generated/dataModel` is the type of one row.

## Indexes on views

Declare [indexes](/database/reading-data/indexes/indexes) on a view with
`.index`, just like a table. The fields must exist in the view's output:

```ts bijection/schema.ts {9-10} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
customer_summaries: defineView({
  key: { from: "customers", field: "_id" },
  expression: q
    .table("customers")
    .as("customer")
    .leftJoin(orderTotals, { left: "customer._id", right: "totals.customer_id" })
    .select({ name: q.field("customer.name"), total_cents: q.field("totals.total_cents") }),
})
  .index("by_name", ["name"])
  .index("by_total", ["total_cents"]),
```

Then query the view with `withIndex`:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const topCustomers = await ctx.db
  .query("customer_summaries")
  .withIndex("by_total", (q) => q.gte("total_cents", 1_000_000n))
  .order("desc")
  .take(10);
```

Only ordinary indexes are supported. Search indexes, vector indexes and staged
indexes can't be declared on a view.

<Tip>
  For a [virtual view](/views/materialized-views), Bijection can read an index
  range directly when the view passes the indexed fields through unchanged from
  its source table and that table has an index on the same fields. Otherwise,
  for example when an index orders by a computed or joined field, it evaluates
  the whole view within its [work limits](/views/defining-views#work-limits) and
  sorts the result. For large views ordered by computed fields, materialize the
  view.
</Tip>

## Views are read-only

A view's rows come from its expression, so they can't be written directly.
`insert`, `patch`, `replace` and `delete` on a view are type errors, and fail at
runtime with a `ReadOnlyView` error even from internal functions. To change a
view's rows, change the tables it reads.

## Consistency and reactivity

Views follow the same rules as every other read in Bijection:

* **Consistency.** All reads in one function, including every table a view
  reads, happen at the same snapshot. A view never combines inputs from
  different moments.
* **Reactivity.** A query depends on everything the views it reads depend on.
  When a write changes the result, including a new row that starts to match a
  filter or a change to a joined table, subscribed clients get the new result.
* **Read your writes.** Inside a mutation, a view reflects the mutation's own
  earlier writes. If you insert an order and then read `customer_summaries`, the
  new order is counted. If the mutation throws, none of it is visible to anyone.
* **Conflicts.** Mutations that read a view are protected by
  [optimistic concurrency control](/database/advanced/occ) over the view's
  inputs, including rows that would newly match an empty range.

This holds for virtual and [materialized](/views/materialized-views) views
alike.

## Limits and errors

Reading a view counts toward the ordinary
[read limits](/functions/error-handling/error-handling#read/write-limit-errors)
of the function, and toward the view's own
[work limits](/views/defining-views#work-limits). A view read can fail with
these errors:

| Error | Cause |
| - | - |
| `ViewWorkLimit` | The evaluation read more rows or bytes than the view's `limits` allow. |
| `ViewCardinalityViolation` | A `cardinality: "one"` join found more than one matching row. |
| `ReadOnlyView` | A mutation tried to write to the view. |
| `InvalidAggregateValue` | A `q.sum` or `q.percentile` met a non-finite number. |
| `InvalidDecimal`, `DecimalOverflow` | A [decimal aggregate](/views/joins-and-aggregates#exact-decimal-and-distribution-aggregates) met a value it can't represent exactly. |

A failed read returns no partial result. Handle these like other
[errors in queries](/functions/error-handling/error-handling#errors-in-queries).
