Skip to main content
You read a view exactly like a table: with ctx.db inside a query or mutation. There’s no separate client or query language.
bijection/customers.ts
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 to db.get:
bijection/customers.ts
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, ordering and retrieving results:
Views also support pagination, including usePaginatedQuery on the client:
bijection/customers.ts
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 on a view with .index, just like a table. The fields must exist in the view’s output:
bijection/schema.ts
Then query the view with withIndex:
Only ordinary indexes are supported. Search indexes, vector indexes and staged indexes can’t be declared on a view.
For a virtual view, 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 and sorts the result. For large views ordered by computed fields, materialize the view.

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 over the view’s inputs, including rows that would newly match an empty range.
This holds for virtual and materialized views alike.

Limits and errors

Reading a view counts toward the ordinary read limits of the function, and toward the view’s own work limits. A view read can fail with these errors: A failed read returns no partial result. Handle these like other errors in queries.