Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/sketch/src/exec.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Serving-time execution model for the L4 IR — the counterpart to
//! `asap_plan::bind`'s planning-time `QueryExpr -> L4Node`. See
//! `docs/l4node-execution-model.md` for the design (planning vs. serving
//! `docs/l4-summary-bound-ir.md` for the design (planning vs. serving
//! L4, the nested-composition rules, the open questions); this module is
//! the implementation of it.

Expand All @@ -12,7 +12,7 @@ use crate::expr::{L4Node, SummaryExpr};
use crate::sketch::{SketchQuery, SummaryKind, SummaryParams};

/// Deployment-supplied backend for executing an [`L4Node`] tree against
/// materialized state — see `docs/l4node-execution-model.md`.
/// materialized state — see `docs/l4-summary-bound-ir.md`.
pub trait SummaryExecutor {
/// Reference to one materialized summary instance (e.g. a sid).
type Handle: Clone;
Expand Down
1,722 changes: 207 additions & 1,515 deletions docs/design.md

Large diffs are not rendered by default.

120 changes: 0 additions & 120 deletions docs/intent-algebra-reconciliation.md

This file was deleted.

69 changes: 69 additions & 0 deletions docs/l1-query-language.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# L1 — query language

Per-language front ends, one per supported query language. Each turns a
raw query string into that language's own native shape — whatever
representation is most natural for that language (e.g. a parsed AST,
or an already-planned relational tree) — with no summary awareness and
no shared vocabulary yet; that only starts once every front end reaches
L2.

Front ends never depend on each other: each language's parsing path is
fully independent, so adding or removing a supported language never
touches any other front end.

A single query language may support more than one concrete surface
dialect at this layer — different syntax that still elaborates to the
same native representation. Dialect differences are purely a parsing
concern; they never affect what a later layer can plan.

## Parse → lower: converging on one shape

Different front ends can look nothing alike at L1 — one may start from
a bare AST, another from an already-planned tree — but every front end
must converge on the *same* L2 shape, through the *same* L2 entry
point. That convergence, not the L1 parsing itself, is the part that
matters at the design level:

```
Query string (language A) Query string (language B)
│ language A's own parser │ language B's own parser
▼ ▼
language A's native representation ← L1 language B's native representation ← L1
│ interpret into shared vocabulary │ interpret into shared vocabulary
▼ ▼
shared logical plan ← L2 — same type for every language
columns are still named references
│ one shared L2 → L3 step, for every language:
│ 1. bind — name → schema-position resolution
│ 2. convert — purely structural L2 → L3 translation
│ 3. canonicalize — shared cross-language normalization
canonical intent tree ← L3 — columns are positions
+ a self-contained schema on every scan
```

Binding is not itself a layer — it's the pass that sits on the L2 → L3
edge. *Why* the canonical form uses positional column identity, how
binding differs between a schema-less source and a catalog-backed one,
and what a unique key is for are all L2/L3 design questions, covered in
[`l2-logical-plan.md`](./l2-logical-plan.md) and
[`l3-intent-algebra.md`](./l3-intent-algebra.md) — this section only
needs to establish that every front end feeds the identical mechanism.

### The nesting contract

Nesting — an operator tree inside a source clause or a function
argument, an aggregate over an aggregate, a binary operation over two
subtrees, a range function over a sub-query, and so on — is required to
lower **structurally**: the canonical intent tree is a recursive,
arbitrarily-nestable structure, so "an operation over a sub-query"
needs no special-cased IR shape. Any language whose grammar allows
nesting must be able to express it this way, without a parallel
representation for the nested case.

Not every syntactically-expressible nesting shape has to be supported.
A front end is expected to cleanly reject a shape it can't yet
represent — rather than silently mis-lowering it — since a
resolvable-later gap is one design choice away from becoming a
correctness bug if it's lowered wrong instead of rejected outright.
88 changes: 88 additions & 0 deletions docs/l2-logical-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# L2 — logical plan

Every front end's output converges on one shared relational tree (or
DAG) type at this layer — not one type per language. Still no summary
names, no positional column identity — those are L2 → L3 concerns
(below). The tree covers the standard relational vocabulary — source
scan, filter, projection, aggregation (grouping + aggregate functions),
windowing, deduplication, heavy-hitter ranking, set/join operators,
ordering, and named/referenced sub-plans (for expressing shared
sub-computations) — plus a small number of extension points for
per-language surface features that don't yet have a general relational
equivalent. Column references are still symbolic names at this layer,
resolved to positions by binding, below.

## Name resolution: binding

Binding walks the L2 tree and produces one self-contained schema that
every column reference in the resulting L3 tree indexes into: it seeds
columns from whatever catalog is available (or a minimal always-present
floor if the catalog knows nothing), and accounts for any name the
query references that the catalog didn't already know about. A schema
records whether it's a *complete* enumeration of a row's columns or an
*open* superset a runtime row may exceed — different query languages
sit at different points on that spectrum (a language with no fixed
schema, only label-like references, versus one with a declared
catalog) and binding accommodates both without forcing them into the
same shape.

Why isolate name resolution as its own pass, rather than resolving
inline while translating:

- **Everything downstream becomes purely structural and total.** Once
a schema exists, later passes work over positions, not names — a
column reference can't fail to resolve once binding has already run,
so every "column not found"-shaped failure is concentrated in one
place.
- **Schema/catalog policy is swappable independently of translation.**
Binding is generic over where schema information comes from; a
language with no catalog and a language with a real one both flow
through the same binding step, differing only in what the catalog
supplies.
- **It's the natural home for resolution-policy questions.** A
reference to "every column except these" can only be resolved once a
schema is known — and for an open schema, the full complement can't
be enumerated at all, so that has to be represented and deferred
rather than resolved eagerly.

Each independent sub-tree (e.g. either side of a binary operation)
binds against its own schema, since the two sides may reference
entirely different sources — but a side must still see names
referenced by an *enclosing* operation (a grouping key mentioned above,
but not inside, either branch), so an enclosing scope's referenced
names are threaded down into each side's own binding pass.

## The L2 → L3 step

One shared step — used by every front end, regardless of language —
does three things in order: bind names to schema positions (above),
translate the tree structurally into L3's own shape, then run a shared
cross-language normalization pass so that semantically equivalent
queries, from any supported language, arrive at the same canonical
shape (covered in [`l3-intent-algebra.md`](./l3-intent-algebra.md)).
This ordering matters: normalization operates on already-resolved,
already-translated trees, so it never has to reason about per-language
surface syntax.

## Column identity: two sources of truth

A source may arrive at L2 in one of two shapes: schema-less (known only
by whatever columns a query happens to reference) or already
schema-carrying (arriving with a declared, complete column set from an
external catalog). Binding accommodates both:

- A **schema-less** source falls back to binding's own usage-derived
schema — open, since a runtime row may carry more than what the
query referenced.
- An **already schema-carrying** source keeps its own declared schema
as-is — closed, since the catalog is a complete enumeration — and
binding's own fallback computation for that source goes unused.

Either way, every column reference downstream resolves to a position
into *some* schema, uniformly; the difference is only where that
schema came from. This asymmetry also carries into whether a source
declares a **unique key** (a set of columns that together identify a
row): an already-cataloged source can carry a real one through
unchanged, while a usage-derived schema has no way to prove one, and so
never gets one. Why a unique key matters is covered in
[`l3-intent-algebra.md`](./l3-intent-algebra.md#unique-keys-and-cross-query-sharing).
Loading
Loading