Expand description
The read model’s relational half: a select compiled into the plan.
docs/99-the-data-tier-means-of-combination.md
§99.9 item 9:
The read-model SQL grows joins and
group byby compiling into the plan, not by growing its own interpreter — which closes §23.19 and §12.5 together and keeps one code path.
§What this module is, in one sentence
It writes the Beck expression a person would have written, and hands it to
crate::plan::Plan::of_query. Nothing here joins anything, groups anything or deduplicates
anything: select … join … on b.k = a.k becomes the loop for a in as: for b in bs where b.k == a.k, which crate::relate already reads as an equi-join over an index, and group by g becomes the loop over list_unique that corpus/35-workload.beck writes by hand. The
operators are crate::plan::Op::Join, crate::plan::Op::ArrangeBy,
crate::plan::Op::GroupBy and crate::plan::Op::Distinct — the same ones, with the same
delta rules, that a program’s view compiles to.
That is the whole of item 9, and it is worth naming what the alternative would have cost: a
second join, a second set of aggregates and a second distinct living beside the first,
agreeing by inspection rather than by construction, and a differential harness that covers one
of the two.
§The shape each query compiles to
| SQL | The expression |
|---|---|
from a join b on b.k = a.k | concat_lists(map_list(a, λx. map_list(filter_list(b, λy. y.k == x.k), λy. row))) — one stage per join, left-deep, so each stage is its own map_list in the plan and therefore gets its own join and its own index |
group by g | map_list(list_unique(map_list(R, λr. g(r))), λk. …), each aggregate a question about filter_list(R, λr. g(r) == k) |
count(*) per group | list_len of that filter — the join’s own tally, so no group is built |
min/max/sum(c) per group | list_min(map_list(that filter, λr. r.c)) — crate::plan::Op::GroupBy |
distinct | list_unique of the projected row |
an aggregate with no group by | the aggregate of the whole collection, as one row |
§What a column is, and why the rows are normalised first
A table’s columns are a schema fact; a table’s element is a run-time value — a record for a
collection of models, the element itself for a collection of scalars. Rather than teach the
compiled expression that difference, every table’s rows go through Table::row_values first,
which is the function Table::row builds a scan’s cells with. So a column is c{n} inside
the plan, for every table, and the scan and the join cannot disagree about what a column is.
§What is deliberately not here
- No cost-based ordering. The
fromlist fixes the left-deep order, exactly as aforloop fixes it (docs/99§99.8’s “an inferred surface postpones the solver”, arrived at from the other side: a written surface fixes the order outright, so there is still nothing for a solver to choose). - No outer join, because an unmatched row would need columns invented for it.
- No
having: awherenarrows the rows before they are grouped, and a filter over the groups themselves would be a second predicate language.
Structs§
- Compiled
- A
select’s relational half, compiled and ready to run.
Functions§
- compile
- Compile a
selectinto a plan over the tables it names. - compile_
with - The same, with
Relatesaid out loud. - relational
- Whether a
selectneeds the plan.