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

# Defining Views

> Declare a view with defineView, a key and a q expression

A view is a read-only, keyed collection that Bijection computes from other
tables in your schema. You declare it next to your tables in
`bijection/schema.ts`:

```ts bijection/schema.ts {9-15} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineSchema, defineTable, defineView, q } from "bijection/server";
import { v } from "bijection/values";

export default defineSchema({
  customers: defineTable({
    name: v.string(),
    region: v.string(),
    is_archived: v.boolean(),
  }),
  active_customers: defineView({
    key: { from: "customers", field: "_id" },
    expression: q
      .table("customers")
      .filter(q.eq(q.field("is_archived"), false))
      .select({ name: q.field("name"), region: q.field("region") }),
  }),
});
```

`active_customers` has one row for each customer that isn't archived, with the
fields `name` and `region`, plus the system fields `_id` and `_creationTime` of
the customer it came from.

Read on to learn how keys and expressions work.

## The `defineView` constructor

`defineView` takes an object with these fields:

| Field | Required | Description |
| - | - | - |
| `key` | Yes | The table whose document IDs identify the view's rows. See [Keys](#keys). |
| `expression` | Yes | A relation built with the `q` builder, starting from `q.table(...)`. |
| `limits` | No | The work one evaluation may do. See [Work limits](#work-limits). |

Register the view in `defineSchema` under the name you'll read it by. The name
shares its namespace with your tables.

A view has no validator of its own. Bijection infers the view's document type
from its expression and the validators of the tables it reads, so
`Doc<"active_customers">` in your generated types is:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  _id: Id<"customers">;
  _creationTime: number;
  name: string;
  region: string;
}
```

## Keys

Every view row is an object with a stable identity: the `_id` of one document in
the key table. The key is written as `{ from: "<table>", field: "_id" }`, and
`_id` is the only supported key field.

The key table must be the table the expression starts from. Bijection follows the
expression's main chain, through `.as()`, `.filter()`, `.select()` and
[joins](/views/joins-and-aggregates#joins) with `cardinality: "one"`, back to the
first `q.table(...)`. If that table is itself a view, the key table is that
view's key table. For a view over `customers`, the key is always
`{ from: "customers", field: "_id" }`.

Because rows are keyed this way:

* A view's `_id` has the key table's ID type, so `Doc<"active_customers">["_id"]`
  is `Id<"customers">`.
* A view has at most one row per key. A customer either is or isn't a member of
  `active_customers`, and a change to its fields can't make it a different
  object.
* Bijection refuses a view whose main chain doesn't preserve one row per key:
  a `groupBy` or a `cardinality: "many"` join directly in the main chain can't
  keep a single source `_id` for each row. Use grouped results on the right side
  of a join instead, as shown in [Joins and aggregates](/views/joins-and-aggregates).

<Note>
  Grouped or compound view keys, where a row is identified by a tuple of field
  values instead of a document ID, aren't supported.
</Note>

## Building expressions with `q`

Import `q` from `bijection/server`. An expression starts with `q.table` and
chains relational operators. Each operator returns a new relation, so you can
build expressions in variables and reuse them.

| Operator | Result |
| - | - |
| `q.table(name)` | Every document of a table or view in the schema. |
| `.as(alias)` | The same rows, nested under `alias`. |
| `.filter(predicate)` | The rows for which `predicate` is `true`. |
| `.select({ ... })` | A projection: one output field per entry. |
| `.leftJoin(right, options)` | See [Joins](/views/joins-and-aggregates#joins). |
| `.innerJoin(right, options)` | See [Joins](/views/joins-and-aggregates#joins). |
| `.groupBy(fields).aggregate({ ... })` | See [Grouping](/views/joins-and-aggregates#grouping). |

### Referring to fields

Use `q.field(path)` to refer to a field of the current row. Nested fields use a
dot-separated path such as `q.field("address.city")`.

After `.as("customer")`, every field of the row lives under `customer`, so you
refer to it as `q.field("customer.name")`. Aliases are how you tell fields apart
once you join two tables:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
q.table("customers")
  .as("customer")
  .filter(q.eq(q.field("customer.region"), "emea"))
  .select({ name: q.field("customer.name") });
```

### Filtering

`.filter` takes a boolean expression. The scalar operators are the same ones
you use in [query filters](/database/reading-data/filters):

| Kind | Operators |
| - | - |
| Comparison | `q.eq`, `q.neq`, `q.lt`, `q.lte`, `q.gt`, `q.gte` |
| Arithmetic | `q.add`, `q.sub`, `q.mul`, `q.div`, `q.mod`, `q.neg` |
| Logic | `q.and`, `q.or`, `q.not` |
| Values | `q.field(path)` and literal values |

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
q.table("orders").filter(
  q.and(
    q.eq(q.field("status"), "open"),
    q.gte(q.field("amount_cents"), 10_000n),
  ),
);
```

Bijection checks the types when you deploy: a filter must be boolean, and
arithmetic needs numeric operands.

### Projecting

`.select` maps output field names to expressions. The output row contains only
the fields you select, plus `_id` and `_creationTime` from the key table:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
q.table("orders").select({
  customer_id: q.field("customer_id"),
  amount_cents: q.field("amount_cents"),
  net_cents: q.sub(q.field("amount_cents"), q.field("discount_cents")),
});
```

Selected field names can't start with `_`. A selected field that can be missing
in the input is optional in the output type.

Literal values keep their exact type. A `bigint` literal is sent as a 64-bit
integer, not a floating-point number.

## Views over views

`q.table` can read another view by name. The outer view is keyed by the same
table as the inner one:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
emea_customers: defineView({
  key: { from: "customers", field: "_id" },
  expression: q
    .table("active_customers")
    .filter(q.eq(q.field("region"), "emea")),
}),
```

A view can't read itself, directly or through other views.

## Work limits

A view's `limits` bound the work of one evaluation: `max_rows` counts rows read,
and `max_bytes` counts the bytes of those rows. The default is 1,000 rows and
1 MiB:

```ts bijection/schema.ts {4} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
order_statuses: defineView({
  key: { from: "orders", field: "_id" },
  expression: q.table("orders").select({ status: q.field("status") }),
  limits: { max_rows: 10_000, max_bytes: 8 * 1024 * 1024 },
}),
```

Both values must be positive integers. A deployment accepts at most 1,000,000
rows and 64 MiB per view. When a view reads another view, the work counts
against both views' limits.

If an evaluation goes over its limits, the read fails with a `ViewWorkLimit`
error instead of returning a partial result. See
[Reading views](/views/reading-views#limits-and-errors).

## What Bijection checks

<Steps>
  <Step title="While you type">
    `defineSchema` checks that every `q.table(...)` in a view names a table or
    view declared in the same schema. A typo fails to type-check with the
    message `view input table is not in this schema: <name>`.
  </Step>

  <Step title="When you deploy">
    [`bijection dev`](/cli/reference/dev) and
    [`bijection deploy`](/cli/reference/deploy) check the complete definition:
    the key, field references and types, join indexes, grouping indexes,
    limits, and that views don't form a cycle. An invalid view fails the push
    with a message naming the problem.
  </Step>

  <Step title="When you read">
    Reads enforce the work limits and the one-row-per-key rule for joins.
  </Step>
</Steps>

## Changing a view

You can change a view's expression, limits and indexes and deploy again. Queries
reading the view see the new definition from that deploy on.

<Warning>
  Once a view is deployed, Bijection refuses a deploy that removes the view from
  the schema or changes its key, with a `ViewOwnershipMigrationRequired` error.
  Declaring a view under the name of an existing table that contains documents
  is refused with the same error.
</Warning>

## What views don't support

A view expression is a declarative plan, not a function. These aren't
available inside an expression:

* Arbitrary TypeScript callbacks or `ctx.db` reads.
* Sorting, limits and top-N selection. Order results when you read the view,
  with an [index](/views/reading-views#indexes-on-views).
* `DISTINCT`, unions, ranking and window functions.
* Joins on anything other than field equality, and right or full outer joins.
* Aggregates other than those listed under
  [Grouping](/views/joins-and-aggregates#grouping).
* Search, vector and staged indexes on the view itself.

For logic a view can't express, write it in a
[query function](/functions/query-functions), which can read views and tables
together.
