Skip to main content
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:
bijection/schema.ts
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: 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:

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 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.
Grouped or compound view keys, where a row is identified by a tuple of field values instead of a document ID, aren’t supported.

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.

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:

Filtering

.filter takes a boolean expression. The scalar operators are the same ones you use in query filters:
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:
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:
bijection/schema.ts
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:
bijection/schema.ts
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.

What Bijection checks

1

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

When you deploy

bijection dev and bijection 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.
3

When you read

Reads enforce the work limits and the one-row-per-key rule for joins.

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

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.
  • 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.
  • Search, vector and staged indexes on the view itself.
For logic a view can’t express, write it in a query function, which can read views and tables together.