Language reference
Relation catalog (relix.relations)
Syntax
relix.relations -- every relation the script can name (name, kind, namespace, materialization, row_count, boundedness)
Description
relix.relations has one row per relation the script can name — the sources it declares, the inline tables it embeds, the connection-backed tables it reaches, and every view it defines with :=.
It is the catalog the others hang off: relix.columns names the same relations one row per column, relix.keys one row per key column, and a query joining any of them to this one is how a question about all the relations gets asked.
Columns:
name(STRING) — the relation's name, as it was written.kind(STRING) — what it is:"SRC"a declared source,"INL"an inline table,"DB"a connection-backed table,"QR"a view,"SYS"an engine-computed catalog. The same codes the IR prints.namespace(STRING) — the namespace the relation was declared in.materialization(STRING) — how much of the relation has to be held at once to produce a row:"stream"(none),"bag","set","sort".row_count(NUMBER) — how many rows it has, where that is known: exact for an inline table or a finite generator, the database's own estimate for a database table. NULL where it is not known.boundedness(STRING) — whether it is finite:"bounded","unbounded", or"unknown".
Two questions about size, and only one of them is measured
row_count and boundedness sound like the same question and are not. A size is observed — counted from the rows of an inline table, known to a generator that knows its own length, or estimated by the database a table lives in, which keeps an estimate for its own planner. Boundedness is proved, from what a relation is and what it reads.
That is why the pairs which look inconsistent are the informative ones:
boundedness | row_count | What it means |
|---|---|---|
bounded | a number | finite, and its size is known |
bounded | NULL | finite, but nobody has counted it — a CSV file nothing has read |
unbounded | NULL | it has no count because it has no end |
unknown | NULL | nothing here shows it finite either way |
Reading a NULL row_count as "empty" is the mistake the two columns together prevent: a table with no count is one nothing has counted.
Where boundedness comes from
A relation that holds its own rows — an inline table, a file, an HTTP resource, a database table, one of these catalogs — is bounded. Each is a finite extent that somebody is holding.
A generator is the exception, and the only leaf that can be otherwise: a generator declares whether it ends, so Range is bounded while Naturals and Primes are unbounded.
A view is neither declared nor measured but derived, from the body it was defined as — so it is exactly as bounded as what it reads, through however many other views:
source Naturals from generator { name: "Naturals" };
Big := { σ n > 1000000 (Naturals) }; -- still unbounded: filtering removes no end
FirstTen := { λ 10 (Naturals) }; -- bounded: λ is what bounds it
source Naturals from generator { name: "Naturals" };
Big := { SELECT n > 1000000 (Naturals) }; -- still unbounded: filtering removes no end
FirstTen := { LIMIT 10 (Naturals) }; -- bounded: λ is what bounds it
That derivation is why the column is worth having on views at all. Selecting from an infinite relation leaves it infinite, and λ is the operator that ends it — the same rule the engine uses when it refuses to sort or group an endless input.
unknown is an answer, not a gap
boundedness is never NULL. "unknown" means nothing here shows this finite either way, which is a claim, where a NULL row_count is the absence of one.
Three things say it. A generator this session cannot place — because no generator of that name is installed. A relation named in a view that resolves to nothing. And a definitional cycle: A defined from B and B from A puts neither on any footing from which finiteness can be shown.
"unknown" does not mean a query over the relation is refused. The engine refuses what is provably endless, and treats not-knowing as permission to proceed — so this column tells you where a query is running on trust.
Technical Description
The extent is fixed when analysis finishes: it describes the script, and querying it runs nothing against a source. A view's boundedness is derived from the same walk the planner uses, over the same lattice — the two cannot disagree about whether a query's input ends.
Like the other catalogs, this one describes only relations outside the reserved relix namespace, which is what keeps it about your script. Use relix.catalog to see the namespace itself.
Examples
Every relation the script can name:
query { relix.relations };
Just the views, in order:
query { τ name (σ kind = "QR" (relix.relations)) };
query { SORT name (SELECT kind = "QR" (relix.relations)) };
What is finite, and therefore safe to sort, group or collect:
query { π name (σ boundedness = "bounded" (relix.relations)) };
query { PROJECT name (SELECT boundedness = "bounded" (relix.relations)) };
The ones to be careful with — endless, or not shown to be otherwise:
query { π name, kind, boundedness (σ boundedness ≠ "bounded" (relix.relations)) };
query { PROJECT name, kind, boundedness (SELECT boundedness != "bounded" (relix.relations)) };
Finite but never counted, which is the pair a NULL count on its own reads wrongly:
query { π name (σ boundedness = "bounded" ∧ row_count IS NULL (relix.relations)) };
query { PROJECT name (SELECT boundedness = "bounded" AND row_count IS NULL (relix.relations)) };
How much of the script is of each kind, and how much of it ends:
query { γ kind, boundedness, COUNT(*) → relations (relix.relations) };
query { GROUP kind, boundedness, COUNT(*) -> relations (relix.relations) };
Which relations buffer their whole input before emitting a row, joined to their size so the expensive ones stand out:
query {
τ row_count DESC (σ materialization ≠ "stream" (relix.relations))
};
query {
SORT row_count DESC (SELECT materialization != "stream" (relix.relations))
};
Limitations
For a database table, row_count is the database's estimate, as current as its last statistics run — not a count, since counting would read the whole table every time a script naming it was analysed. A database that has never gathered statistics for the table gives NULL. For an inline table and a finite generator it is exact. A view never carries one. See Two questions about size above.
boundedness is a lower bound on what is provable, not on what is true. A FIX whose step computes new values can have an endless extent and still read bounded — whether such a recursion terminates is not decidable, so no analysis of this kind answers it. What stops that query is the engine's runtime limit, not this column.
A table-valued function's body is not walked, so a view calling one is derived from the rest of its body alone.
See also
- candidate keys —
relix.keys, the other thing the statistics know about a relation. - introspection stdlib —
relix.schema(rel)andrelix.find(col), the column catalog read from either side. - reserved namespace —
relix.catalog, this relation's counterpart for therelix.*names themselves. relix.catalog— lists everyrelix.*relation.