TessariDBDocs
  • 0.34.2-betalatest
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

Views

A view is a name for a read — expanded where it is used, resolved against the caller's own permissions; a materialized one keeps its answer, current from the change feed.

DEFINE COLLECTION staff;
CREATE staff:1 = { name: 'ada', team: 'eng', salary: 120 };
CREATE staff:2 = { name: 'grace', team: 'eng', salary: 100 };
CREATE staff:3 = { name: 'hopper', team: 'ops', salary: 90 };

DEFINE VIEW engineers AS SELECT * FROM staff WHERE team = 'eng';

SELECT * FROM engineers;

A view is a name for a read. Nothing is stored under it, nothing is maintained, and reading it costs exactly what running its read costs — unless it is declared MATERIALIZED (below). When a statement names a view, the view's read is put in its place before anything else happens, and what runs from there is an ordinary read of ordinary tables.

What you get, and what you filter

A view carries its own projection, and a condition written outside it applies to what the view answered, not to the record underneath:

DEFINE VIEW roster AS SELECT name, team FROM staff;

SELECT * FROM roster WHERE team = 'eng';    -- rows 2
SELECT * FROM roster WHERE salary > 85;     -- rows 0

The second answers nothing, and it is not a mistake: roster answers with name and team, so a condition on salary is a condition on a field that is not there. If that surprises you, the view is the right place to fix it — a view is a read, and reads say what they answer with.

Views over views

A view may name another, and the chain is expanded as far as it goes:

DEFINE VIEW senior_engineers AS SELECT * FROM engineers WHERE salary > 110;

The chain is bounded at eight, and a longer one is refused with ViewsTooDeep rather than followed. A bound is needed because a view can name a view that names it back, and an unbounded expansion of that is a hang rather than an error.

A view holds nothing, and everything follows from that

Four statements, and one refusal for all of them:

CREATE engineers:9 = { name: 'ev' };            -- writing into one
SELECT * FROM engineers:1;                      -- a record address into one
SELECT * FROM engineers:1..3;                   -- a span of identities
DEFINE INDEX by_name ON engineers FIELDS name;  -- an index over one

Refused — ViewIsNotATable, every one of them.

There are no records under a view to write, address, walk or index. Each of these is refused where it is written rather than answering emptily, because an empty answer would read as no matching records when the truth is this is not the kind of thing you asked.

Names

Views and tables share one namespace. A view cannot take a table's name, a table cannot take a view's, and a repeat definition is refused rather than replacing (NameTaken) — so redefining a view is a DROP VIEW and a DEFINE VIEW, done deliberately, rather than a statement that silently changes what everything reading that name gets.

A view's body must be a read. DEFINE VIEW broken AS DELETE FROM staff is refused where it is written.

Whose permissions a view runs with

The caller's. A view is a saved query: the tables it names are checked against the grants of whoever reads it, exactly as if they had written the read out by hand. A grant on a view is refused, because no grant on a view would ever be consulted.

This is the opposite of the definer's-rights view found in some other stores, where a view carries the authority of whoever declared it and is used to hand somebody a narrow slice of a table they cannot otherwise reach. That shape is not here, and the reason is that its failure mode is a SELECT returning rows the caller was never granted, with nothing anywhere in an error state. It also owes a rule for what happens when the definer is dropped, and a choice between the definer's grants at definition time and their grants now. If you need to narrow what a user can reach, narrow it with a grant.

Expansion runs between the parse and the grant check, so the grant check sees the tables the read actually touches rather than a name that stands for them.

Dropping one

DROP VIEW engineers;

The definition goes and the records are untouched, which is the whole difference between dropping a view and dropping a table. DROP VIEW refuses a name of another kind rather than removing it.

A view that keeps its answer: MATERIALIZED

From 0.22.0-beta a view can keep its answer instead of re-reading it every time it is named:

DEFINE VIEW well_paid MATERIALIZED AS SELECT name, salary FROM staff WHERE salary > 95;

SELECT * FROM well_paid;
INFO FOR TABLE well_paid;

It is filled when it is declared, in the declaring transaction, so it is never read empty because it was not built yet. After that every serving node brings it current from its source table's change feed, in the order the writer made the changes — never inside the writing transaction, so a write to staff pays nothing for the views kept over it.

Its rows always equal its read at the version it states. INFO FOR TABLE reports a materialized object: the version the rows equal, how many versions the store has moved behind it, the rows held and when it was last refreshed. Reading the view answers what its read answers at that version, record for record. A view nothing has changed under is brought forward every ten seconds, so behind measures change rather than silence.

What it can keep is one source table that is not a view or a vault, and only a read its source's changes describe — no join, traversal, subquery, FETCH, VERSION, STALENESS, ANSWERED BY, TIMEOUT, AFTER, approximate or fused order, and no function that is not a function of the record (time::now(), rand::uuid()). Each is refused where the view is declared:

DEFINE VIEW recent MATERIALIZED AS SELECT name, time::now() AS seen FROM staff;

Refused — MaterializedShape, naming what it cannot keep.

Who reads it. A kept view is read with the caller's grants on its source: a user who may read the source reads the view, one who may not is refused, and one who may read only some of its fields is refused too (MaterializedFromHidden) — its rows were computed already and cannot be redacted after the fact.

A grouped view's row is its group. From 0.26.0-beta a view whose read groups by GROUP BY — and neither orders, bounds nor splits its groups — keeps each group's row under the group key's order-preserving bytes rather than under its position, so reading it answers in the read's group order and a row keeps its identity while other groups come and go:

DEFINE VIEW by_team MATERIALIZED AS SELECT team, count(*) AS people FROM staff GROUP BY team;
SELECT id, team, people FROM by_team;
0x05656e67000100: { people: 2, team: 'eng' }
0x056f7073000100: { people: 1, team: 'ops' }
(2 record(s), via scan)

Each identity is the encoded group key — eng before ops, as the read orders them.

To know which group a changed record left — a change carries no old value — the view keeps a membership map beside its rows, written in the same commit. A grouped view declared before 0.26.0-beta is rebuilt this way on its first maintenance pass after the upgrade, which is when its row identities change.

What it costs. A view answering one row per record recomputes one record per change, and a grouped view the groups its changes left and joined: 50 000 records in 500 groups, one change per batch, on disk, went from 51.5 to 5.1 ms per batch at the median — about one synced commit. The membership map is two small rows per source record, paid when the view is declared (51 to 630 ms for those 50 000) and on every change after. A view that folds without grouping, or orders, bounds or splits, recomputes its whole read per batch that saw a change, which is the price of an answer that is always exactly its read. Its rows travel in the log like any other write, so a follower holds what its leader kept and a failover moves the maintenance with the leadership.

A series' rollup is the other maintained answer, and narrower on purpose: it keeps per-window count, sum, min and max in the same transaction as the raw write, so it is never behind at all.

Views — TessariDB