The Frame-QL Manual
The formal query language of Columna, by datumwise
Frame-QL First Edition — a renamed, lightly-revised continuation of the Coframe-QL Manual, Third Edition (which it supersedes). The Third Edition’s content is preserved; this edition renames it to Columna/Frame-QL/Manifold and reconciles it to the column-foundation redesign (ADR-031), the Core/Pro split, and the shipped envelope grammar (ADR-035). It inherits the v4 operator model (ADR-009…015), the V/M/B anchor vocabulary (ADR-024), the anchor-ascription rule, family-aware scans, macro bindings, EXPLAIN, and the (anchor, universe) grain (ADR-025…028). The envelope — SELECT <series> [AS <alias>], … AT {anchor} with its optional FROM, WHERE, HAVING, ORDER BY, LIMIT … PER, and WITH clauses, where @ {…} is the input-anchor marker universally and AT {…} is the sole output-grain declaration — is the language, and this edition documents it as implemented (columna-core 0.9.0). The pre-launch shipped surface was a fragment of it — the terse cols @ anchor form, where a trailing @ spelled the output anchor; that fragment is retired (its two @s meant opposite things) and preserved for lineage in Appendix D. The fourth outcome, named inform under ADR-020’s inform-and-serve doctrine, ships on the wire as the refuse mood; this edition uses refuse throughout (Chapter 7). The §2c universe law (structural universe resolution; one expression, one universe; cross_universe), the shipped B3 basis/absence rule, and the shipped reason codes are reconciled into the semantics they govern.
About the examples (stated once): every FrameQL example in this manual is checked against the shipped
parser (columna_core.envelope.parse_statement, columna-core 0.9.0) by docs/tools/check_manual_frameql.py,
which rides the manuals merge as a standing test — the manual can never document syntax its own parser
rejects. A code block fenced frameql-illformed is deliberately ill-formed: it shows a syntax the
parser rejects (a [SCHEDULED]/[ROADMAP] surface not yet shipped, or a teaching non-example), and the
check asserts it does not parse. Every other FrameQL block parses clean (it may still refuse or
clarify at plan time — that is semantics, which the four moods report, not grammar).
Preface
The Frame is the query, not operational instruction. Frame-QL takes its name from what remains when you take the Data out of a DataFrame: the Frame — a collection of column expressions on one anchor, and still everything. In Columna a column is an expression of what a thing is, not a step in a process, and a Frame-QL statement is a declared assembly of such columns at one stated grain — never a sequence of operations to perform. The writer says what the answer is — which series, at which anchor, under which conditions — and the framework decides how to realize it. This is the origin law of the language, and everything below is its consequence: the statement names a frame; the planner assembles the frame and owns the envelope’s clauses; the engine computes the series, envelope-blind. Nothing in the surface syntax describes an operation to run.
Frame-QL is the formal query language of Columna. It is the language compilers compile, agents emit, integrity certificates are stated against, and external systems integrate with. Every query against a Manifold — whether typed by a user, generated by the Columna agent, or produced by sugar-expansion from a higher-level surface — ultimately becomes a Frame-QL expression that the framework parses, typechecks, plans, and executes.
This manual is the reference for that language. It is intended for analytics engineers writing queries directly, developers building tools that integrate with Columna, and anyone who wants to understand precisely what the framework will and will not accept.
A note on scope. This manual specifies the language: its grammar, its semantics, its type rules, and the outcomes it produces — every query returns a (result, annotation) pair, and this manual specifies when the result is served, when it is served with disclosures, and in which narrow cases no result can be produced. It is not a tutorial on Columna concepts — for that, see the Columna Framework manual and the companion Columna Reference Manual. This manual presumes familiarity with the framework’s concepts: columns as functions from anchors to values; the three anchors a column carries — the V-anchor (its domain), the M-anchor (its missingness mechanism), and, per family, the B-anchor (the axes its reducer is blocked from rolling up across); the family set, default family, and the fertile/mule distinction; degeneracy and coverage; defining boundaries; reducers and the three anchor-actions crossed with the order axis; and the four-state integrity certificate. Terms are used here as they are defined there.
The grammar is given formally in the companion Frame-QL BNF Grammar; this manual covers semantics and usage. Where this manual and the BNF differ, the BNF is authoritative on syntax and this manual on meaning.
Revision note (Frame-QL First Edition)
This edition is the vetted Third-Edition text, renamed and reconciled. The reconciliations are small and are flagged in place with a ▸ Frame-QL revision note. The ledger:
- Many-to-many aggregate-across (fan-out) is a clarification, not a served disclosure (Chapters 5.6, 7.3, 7.4). Under the column-foundation (ADR-031) the engine transports along functional edges and never joins; a non-functional (M:N) edge is therefore not a traversable path, and the membership aggregation the Third Edition served is not expressible without a declared resolution. The unmet precondition is reported, statically and before execution, as a clarification naming the same three remedies. This replaces “serve membership with overlap disclosure” for the undeclared case.
- The inform-and-serve boundary is the planner/engine boundary (Chapters 5, 7.2). Everything statically catchable — ambiguity, unknown operator, unknown column, type mismatch, non-traversable edge, governance/access — is decided by the planner, before resolution, and may yield a clarify/refuse with no result. Analytical risk that survives to the column engine is served-and-disclosed, never withheld. A B-anchor crossing is detected at the planner (it attaches the critical disclosure) and may be downgraded to a route-around at the engine (Chapter 5.3); the number is always served.
- Mule recompute is recompute-from-base at the output grain where no fertile decomposition exists (Chapter 7.4, Appendix A) — stated explicitly, consistent with the Third Edition.
- Universe depth. The
(anchor, universe)grain and the basis-driven empty-bucket rule are unchanged at the language surface (Chapter 1.5); beneath the surface, Columna enriches universe with a population-consistency layer (support reconciliation across a universe’s columns; path-independence of a reduced total across anchorings). This is semantics under the syntax, not new syntax. - The envelope is the language; the terse
@-fragment is retired (Preface; Chapters 1–4; Appendix D). The pre-launch surface spelled the output anchor with a trailing@(aov @ cal.month); the ratified envelope (ADR-035) makes@ {…}the input-anchor marker universally andAT {…}the sole output-grain declaration, and adds theFROM/WHERE/HAVING/ORDER BY/LIMIT … PER/WITHclauses. This edition documents the envelope as implemented; the terse form moves to Appendix D (lineage).FROMis optional — it defaults to the bound Manifold and is required only where a surface holds more than one (Chapter 1.3). - The fourth mood is
refuse, notinform(Chapter 7). The shipped wire’s four moods are serve · disclose · clarify · refuse. The withholding outcome this manual’s Third-Edition lineage called inform (ADR-020’s “inform-and-serve” doctrine) is the wire’srefuse; the doctrine’s spirit — withhold only structurally, disclose analytical risk — is unchanged, and this edition usesrefusethroughout. (The separatecross_universecategory error rides the query-error channel, not a mood — Chapters 2, 7.)
Core and Pro
Columna ships in two tiers, and this one manual covers both. Constructs available only in Frame-QL Pro are tagged inline [Pro]; everything untagged is in Frame-QL Core. Core is a single-backend, transport-based engine with the full query surface, the reducer/scan/map operator umbrella, V/M/B anchors, universes, families, B-anchor disclosure, and EXPLAIN. Pro adds the missingness behavior map and MISSING_POLICY, coverage states, WITH allocation, the sketch reducers and sketch types beyond the Core set, multi-backend/federation, and custom operators/types. The tagging is a serialization view over one source of truth, exactly as disclosure_default is (Chapter 7.1): it narrows what a tier exposes, never what the language means.
Chapter 1. Basic Structure
1.1 What a query is
A Frame-QL query produces a single output frame. A frame is an assembly of co-anchored value columns at a single anchor; the anchor’s dimensions appear automatically as the leading columns of the frame, and the value columns are exactly the series the query computes. This is the framework’s frame model from the conceptual manual, expressed at the language level: the query says what Manifold to address, what series to compute, at what anchor the result lives, with what filters and ordering. The result is a frame.
1.2 The skeleton
The complete skeleton of a Frame-QL query — the envelope (ADR-035), given here in its fixed clause order:
[EXPLAIN]
[FROM <manifold>]
[WITH <name> = <expression> [, ...]]
SELECT <series_1> [AS <alias_1>], <series_2> [AS <alias_2>], ...
AT { <output_anchor> }
[WHERE <per-series predicate> [AND ...]]
[HAVING <output-frame predicate> [AND ...]]
[ORDER BY <output-frame column> [ASC|DESC] [, ...]]
[LIMIT n [PER { <anchor coordinates> }]]
SELECT and AT are required; every other clause is optional. FROM is optional and defaults to the bound Manifold — the surface the session or tool is attached to — and is required only when a surface holds more than one Manifold and the statement must name which (Chapter 1.3). ON is not a query clause — a population is pinned in definitions, and universe is resolved structurally at query time (Chapter 1.5, the §2c law); an ON in a query is a syntax error. Clause order is fixed: the clauses are optional but may not be reordered, and a misordered clause is reported as a syntax error naming the expected order. A statement may prefix the whole query with EXPLAIN (Chapter 1.7), which validates and plans without executing. Each clause has a precise role developed in the following chapters; the important thing here is the overall shape — a query declares a frame, one structural aspect per clause.
The query produces a frame whose anchor is the output anchor named in AT, whose value columns are the series listed in SELECT, and whose rows are determined by the data, the filters, the ordering, and the limit.
1.3 The FROM clause
A query may name its Manifold explicitly (this is the FROM clause shown alone, not a whole query):
FROM finance_manifold
FROM is optional, and defaults to the bound Manifold. The demo, the MCP query tool, and the agent are each attached to exactly one Manifold, and against a single-Manifold surface a bare SELECT … AT {…} resolves against that bound Manifold — no ceremony on the common case. FROM earns its keep on a multi-Manifold surface: where a surface holds more than one Manifold, a statement that names none is not underdetermined-by-guess, it refuses, naming the available Manifolds and asking which. The clause exists so that any surface — a query console, an agent, an MCP client, a planner shared across several Manifolds — can address different Manifolds by naming the target in each statement, without carrying Manifold identity as ambient session state. A statement that names its Manifold is self-contained: it means the same thing regardless of who submits it, from where, with what session history — and is therefore reproducible, cacheable, and auditable on its face; and an unnamed one is reproducible all the same, because the resolved identity always rides in the annotation (below).
Resolved identity is never silent. A Manifold is a versioned artifact, and the result’s annotation always records the resolved identity — (Manifold, version, refresh, certificate state) — so that even a statement that named neither the Manifold nor a version is exactly reproducible after the fact, from its own annotation. An EXPLAIN shows the resolved identity before anything runs. (▸ Frame-QL revision, shipped-law — [SCHEDULED]: explicit version pinning — a VERSION n on FROM —
is designed but not yet in the shipped envelope grammar; it enters by ruling (ADR-035 D1), not by
default. [SCHEDULED] (not [ROADMAP]) because the surface is designed and committed, only unshipped —
the resolved version is always disclosed today, so nothing is lost by pinning arriving later.)
Surfaces may supply FROM on the user’s behalf — licensed by disclosure. A user or client working against a single Manifold should not have to type its name in every statement, and the formal language does not bend to spare them: the resolution is the surface’s job. A surface whose context makes the target Manifold unambiguous injects the FROM clause before submission (the sanctioned sugar of Chapter 8.3). What licenses the injection is the disclosure machinery: the annotation’s canonical form carries the statement as executed, FROM and resolved version included, on every round trip — so the convenience can never become hidden state. The user need not know the clause exists; the record always shows it.
Injection is convenience, never authorization. A surface that exposes one Manifold makes others unaddressable through that surface — which can feel like access control, and is not. A programmatic client against the same planner may write any FROM it likes. Entitlement is enforced at the governance layer — the Manifold author’s access rules (the reserved ACCESS construct; an unentitled statement is the refuse outcome of Chapter 7.3) — and the surface’s injection sits strictly on the ergonomics side of that line.
Cross-Manifold analysis happens through composition, not through cross-Manifold queries: two Manifolds are composed into a third (by inheritance or joining, per the framework manual’s Chapter 8), and the composed Manifold is then queried like any other. There is no inline cross-Manifold join construct in the language — and a shared planner does not change this: it routes each statement to exactly one Manifold; the multi-Manifold question is answered by composing first and querying the composite.
1.4 The SELECT clause and the output anchor
The SELECT clause lists the series expressions that will become the value columns of the output frame, and ends with AT <output_anchor> declaring the output anchor of the whole query:
SELECT sum(revenue @ {transaction}),
sum(cost @ {transaction})
AT {customer}
Several structural points are worth being explicit about, because they govern how the language reads:
Series expressions are output columns. Each series in the list becomes one value column of the output frame. There is no other way for a series to enter the result — the SELECT list literally names the value columns of the frame.
The output anchor is a single declaration for the whole query. All series in a query share the output anchor, by structural necessity: the result is one frame, a frame’s columns are co-anchored, so all the value columns in the result are at the one declared anchor. The AT clause is therefore a query-level declaration, not a per-series one. There is no syntax for series at different output anchors in one query — different anchors require different queries.
The output anchor’s dimensions appear automatically as leading columns. A query at AT {customer, region} produces a frame whose first columns are customer and region — the anchor coordinates that identify the rows — followed by the value columns computed by the series expressions. The writer does not list anchor dimensions in SELECT; they are implied by AT and produced automatically.
The output anchor is required. A query without an AT clause is ill-formed and refused. The framework does not infer an output anchor from context, does not default to {}, and does not guess. (A specific tool or surface may choose to apply an implicit default at its own discretion — typically by interpreting “no anchor” as AT {} for ad-hoc exploration — but this is tool-level sugar, not part of the language. The formal language requires the anchor to be named.)
1.5 AT and @: output anchors versus input anchors
Every anchor specification in the language is one construct with one meaning, written with one of two keywords depending on position. The construct is anchor ascription:
expr @ ameans the value ofexpr, materialized at anchora.
The writer states where the value is wanted; the framework supplies whichever anchor-action realizes the ascription — aggregation through the relevant family when the expression’s grain is finer than a, broadcast (replication) when it is coarser, identity when they are equal. The legitimacy of the realization is checked by the ordinary machinery (the family’s B-anchor, the verified hierarchy edges, the order requirements); the ascription itself never names the mechanism, only the destination.
The destination, stated in full, is a pair. An anchor names a coordinate signature; it does not by itself determine which points inhabit it — two columns addressed by the same coordinates can live on different declared universes (the expected hourly readings and the alerts that happened to fire are both at {sensor, hour}, and are not about the same points). Every ascription is therefore implicitly expr @ (a, U): the anchor names the coordinates, the universe defines the points. The universe is resolved STRUCTURALLY, never pinned at query time (the §2c universe law): every column expression evaluates inside exactly one universe, and a single expression that spans two populations is a cross_universe category error, not a hedged answer. Which universe a column belongs to is fixed in its definition — ON is a definition-language clause, not query syntax — so a query never re-declares a population; it is disclosed: the resolved (anchor, universe) of every series rides in the result’s annotation. A frame that genuinely needs several populations juxtaposes them — separate columns, each evaluating in its own universe, standing together at a shared anchor (Chapter 2.4).
The choice of universe is consequential because a universe declares its basis, and the basis fixes what a missing row means (the shipped B3 law): in an events population, absence is a lawful zero — a SUM/COUNT over nothing is a true zero, immaterial; in a spine or product population, absence is a gap — the figure covers what is present, and the frame carries an incomplete_data caveat, never a silent zero; in a registry, membership is a checkable fact (present vs absent). Serving follows the declared basis; the resolved basis and any gap ride in the annotation. (The full universe model — bases, declarations, co-universality — is the framework manual’s; this manual specifies only what the query language exposes: structural universe resolution, the one-expression-one-universe law, juxtaposition, and the disclosure of the resolved (anchor, universe) and basis.)
This single rule covers every appearance of an anchor in the language. revenue @ {transaction} ascribes a column to its grain. sum(revenue @ {transaction}) @ {customer, month} ascribes a reduction’s result to an intermediate grain. revenue @ {} in a share-of-total ascribes a column to the Manifold scalar, realized by aggregation, then consumed at a finer grain, realized by broadcast. These are not three constructs; they are one construct in three places.
The two keywords mark the one structural distinction that remains:
@ writes an ascription in input position — the expression’s value at the named grain feeds a larger expression.
AT writes the one ascription that is nobody’s input — the query’s final output anchor. It appears exactly once per query, at the end of the SELECT list, and applies to all series.
The two are not interchangeable, and the framework refuses queries that use @ where AT belongs or vice versa — not because they mean different things (both are ascriptions) but because the query’s final output is structurally unique, and the language keeps that uniqueness lexically visible.
1.6 Series names and the AS alias
Each series in a query’s SELECT list becomes a value column of the output frame, and the column needs a name. A series expression may declare its name explicitly with AS:
SELECT revenue AS total_revenue,
(revenue - cost) AS profit
AT {customer}
When AS is not provided, the framework names the column by its canonical expression — verbatim, not a mechanical rewrite. A column’s identity is what it is: either the name you give it with AS, or the expression itself. No name is invented, and none is mangled.
Bare column reference. The column’s own name. revenue → revenue.
Member access. The dotted reference, verbatim. revenue.sum → revenue.sum (not revenue_sum — the framework does not rewrite the dot; the key is the expression).
Single reducer applied to a column. The reduction, verbatim, input anchor and all. avg(revenue @ {day}) → avg(revenue @ {day}); max(revenue @ {customer*month}) → max(revenue @ {customer*month}). The input anchor is part of the column’s identity (per the Two Anchors law), so it appears in the key — two reductions that differ only in their pin are different columns with different keys, never a collision on an invented name.
Any other expression. No derivable identity. The writer must supply AS. This includes:
- Arithmetic and other map expressions:
revenue - cost,revenue / orders. - Conditional expressions:
if(...),case ... end. - Bracket-filtered column references:
revenue[region = "east"]. - Composite reductions:
max(sum(revenue @ {transaction}) @ {customer, month}).
For these the canonical expression is not a single-atom identity the framework will key on, so the query is ill-formed without AS.
Name collisions are refused, not silently resolved. If two series in the same query resolve to the same key — the same canonical expression with no AS on either — the query is refused with a structured error naming the collision. The framework does not append disambiguating suffixes or otherwise guess; the writer must declare an alias for at least one of the colliding series. The same rule applies if a series’s key (canonical or aliased) collides with an anchor dimension’s name (anchor dimensions appear automatically as leading columns of the output frame and own their names).
Keying on names across versions. Because an unaliased column’s key is its canonical expression, it will change if you rewrite the expression — and the mechanical defaults of earlier versions (sum_revenue, revenue_sum) are gone (contract_version "2", 0.14.0). A consumer that needs a stable handle should key on an AS alias: an alias is author-owned and will never change under any future rule.
Alias visibility. An alias declared in SELECT is visible in:
HAVING, which references series by name to filter post-reduction.ORDER BY, which references series (or anchor dimensions) by name to sort.LIMIT n PER {dims}, where thePERdimensions must be anchor coordinates (and thus appear by their dimension names, not aliases).
An alias is not visible in:
WHERE, which is evaluated before reduction; the series do not exist yet, and the alias has no meaning.- Other series expressions in the same
SELECT. Each series is computed independently from the inputs; a series cannot reference another series’s alias. Writers wanting reuse must repeat the expression or use a tool-level sugar that expands to repeated expressions.
Valid alias names. An alias is an identifier subject to the same rules as a column name: alphanumeric with underscores, not starting with a digit, not a reserved keyword. The alias must not collide with anchor dimension names, with another series’s alias or default name, or with reserved identifiers in the Manifold’s namespace.
The aliasing rules require explicitness exactly where the canonical expression is not a single-atom identity (composite expressions, bracket filters, name collisions) and key by the expression itself everywhere else (bare columns, member access, single reductions). The writer is asked to name a series only when the framework cannot identify it by its own expression.
(One mechanism deliberately complements the alias rules: the macro binding of Chapter 4.5, WITH name = expression, which provides the expression reuse that aliases do not — an alias names an output column after reduction; a macro names an expression template before everything.)
1.7 Statements: queries and EXPLAIN
A statement is a query, optionally prefixed with EXPLAIN:
EXPLAIN
FROM finance_manifold
SELECT max( sum(revenue @ {transaction}) @ {customer, month} ) AS peak_month
AT {customer}
EXPLAIN performs everything short of execution — parse, macro expansion, desugar to canonical form, type-check, plan, certificate consultation — and returns, instead of a result frame:
- the canonical desugared form of the query, every sugar expanded, every inferred anchor and reducer made explicit;
- the planner’s atom decomposition: the single-output metric atoms of the form
op(input @ a_in) @ a_outthe plan comprises, and the planner-level map expressions evaluated over their co-anchored results (Chapter 2.1); - the dependency cone — the integrity preconditions the query relies on, each with its current verdict;
- the complete would-be annotation: every disclosure the executed query would carry, and any clarification it would raise.
EXPLAIN never touches data. It is the validation surface of the propose-validate-refine loop: for an agent, the cheap inner loop that turns the substrate’s checking into a feedback signal before any execution cost; for a human, the audit of exactly how the framework read the query. A statement that EXPLAINs clean executes identically when resubmitted without the prefix — same canonical form, same plan — modulo certificate changes between the two calls.
Chapter 2. Series Expressions: The Canonical Form
This chapter specifies the series — one output column’s expression — in its fully-explicit canonical form: the form the planner type-checks against, where every reduction carries its input anchor @ {a_in} and the statement carries its output anchor AT {A}. The sugars of Chapter 3 let the writer omit an input anchor when it is determinable, and desugar to this form; the canonical form is the bedrock. Under the envelope (ADR-035, Chapter 1), @ {…} is the input-anchor marker universally and AT {…} is the sole output-grain declaration — one meaning for each marker, everywhere.
2.1 The shape of the canonical form
Canonically, a series is a reduction (or a map, or a composite of both) over column references, each reference carrying its input anchor, and the whole statement carrying one output anchor:
SELECT op( col_1 @ {a_1}, col_2 @ {a_2}, ... ) AS name
AT { A }
Every grain is named: every reduction reads an explicit input grain and produces the output grain A; a nested subexpression carries its own input anchor. The framework parses this form directly, type-checks it, and plans it. The sugars of Chapter 3 are normalizations to it.
Two facts give the canonical form its standing, and they are two views of one thing.
Semantic: under the anchor-ascription rule (Chapter 1.5), every sub-expression of a canonical-form statement has a local, self-contained meaning — this value, at this anchor — so the meaning of the whole is the composition of the meanings of the parts, with no ambient context to consult.
Architectural: the canonical form is the planner’s decomposition target. The planner receives a statement, expands and desugars it, and decomposes the plan into atoms — single-output metric requests of exactly the canonical shape, op(input @ {a_in}) @ {a_out}, each carried to the metric engine as a typed retrieval specification (the operator name, the input, the input and output anchors, any order specification, the resolved missing-behavior) — plus the map expressions the planner evaluates itself over the atoms’ co-anchored results. The metric engine answers each atom from its cache or by issuing the few specific backend calls; the engine never sees the statement, only atoms, and is envelope-blind — the planner owns assembly and the envelope’s clauses. A canonical-form statement is therefore not merely a fully-explicit notation: it is its own execution decomposition, readable off the text. This is why the canonical form is the bedrock — it is simultaneously the type-checked meaning of the statement and the unit of planning — and why EXPLAIN (Chapter 1.7) can return the atom decomposition as a faithful account of what execution will do.
2.2 Bare names: inputs versus complete series
A name like revenue may be one of two things, distinguished by where it appears:
As an input to an outer expression — for example, revenue inside sum(revenue) AT {A} — a bare name names the column’s family. It refers to the column under its default family’s reducer, sourced from the column’s root. The outer expression consumes the family; the framework brings the column from its root to wherever the outer expression needs it, applying the default-family reduction as part of getting there. The input position is well-defined for bare names because the outer expression’s output anchor and the column’s root together determine what is being consumed.
As a complete series with a matching grain — a bare measure standing alone in SELECT is a series iff its grain matches the output anchor AT {A} (or reaches it by a verified edge, carrying an implicit identity reduction). SELECT revenue AT {region} is well-posed when revenue reaches region. A bare measure that would require a reduction to reach AT but names none does not serve: it clarifies input_anchor_ambiguous, which must say at what grain, and how, it collapses. A measure family member is addressed with a dot — level.last, level.sum — the same suffix addressing the anchor grammar uses (Chapter 1); SELECT level.last AT {store} reads the end-of-period inventory snapshot per store.
2.3 Single-series reducers and the input pin
A reducer applied to a column, with an explicit input pin and the statement’s output anchor:
SELECT sum(revenue @ {transaction}) AS gross AT {customer}
SELECT max(revenue @ {transaction}) AS peak_txn AT {customer}
SELECT avg(aov @ {day}) AS typical AT {customer}
The first sums transaction-grain revenue up to customer — the natural extensive aggregation. The second takes each customer’s maximum transaction. The third averages the customer’s daily average-order-value.
The third’s choice of input anchor matters, and this is the central rule the canonical form enforces: when the answer depends on the input grain, the input anchor must be explicit. “Average per customer” is not well-defined without saying what is averaged — daily values? weekly? per-transaction? — and each gives a different number. For fertile reducers on a column with a unique natural grain, the input grain is determined and the sugar of Chapter 3 supplies it; for mules (avg, mean, a rate, a snapshot) and for genuinely underdetermined cases, the pin must be named. When it is omitted and the grain is underdetermined —
SELECT avg(aov) AT {customer}
— the framework does not guess. It clarifies on the shipped reason input_anchor_ambiguous, naming the candidate input grains as substitutable alternatives. Its sibling co_anchor_ambiguous is the multi-input case: a reduction over several inputs whose common input grain is not determined (one reason per contested dimension). An average with no declared input grain is a question, not an answer.
2.4 Map expressions
A map combines co-anchored columns point by point:
SELECT (revenue @ {customer, day}) - (cost @ {customer, day}) AS profit AT {customer, day}
SELECT (revenue @ {transaction}) / (orders @ {transaction}) AS aov AT {transaction}
The operands of a map must be co-anchored — the same input anchor for all. Operands natively at different grains must first be brought to a common grain (by reduction, or by broadcast of a coarse value down a functional edge — §2.6). A map preserves the anchor: co-anchored inputs give a co-anchored result. A map may also appear inside a reducer — sum( (revenue @ {transaction}) - (cost @ {transaction}) ) AT {customer} computes per-transaction profit, then sums to customer grain.
A common anchor is, in full, a common (anchor, universe): columns combined at a shared anchor are checked for co-universality. Combining measures from different universes in one expression is not a hedged answer — it is the cross_universe category error (§2.5), on the query-error channel, not a map failure. To place several populations in one view, juxtapose them (separate series, each in its own universe — §2.5), never combine them in one expression.
2.5 One expression, one universe
Every column expression evaluates inside exactly one universe and never crosses the boundary — the §2c expression law (Chapter 1.5). A single expression that combines measures from more than one population is a category error, returned on the query-error channel as cross_universe (not a mood), naming the two legal paths: ask each population separately, or reconcile them into one. The retired cross-universe “wedge” — a single rate whose numerator and denominator live in different populations — lands here.
A frame in one universe needs no qualification at all: the population is implicit and the frame resolves (single-universe sugar). When several populations genuinely belong in one view, the frame juxtaposes them — separate series, each evaluating in its own universe, standing together at a shared anchor:
SELECT revenue AS revenue,
level.last AS inv
AT {region}
Here revenue evaluates in its population and inv in its own; the frame carries both, each honest about where it comes from. ON is not part of the query grammar — a population is pinned in definitions, never re-declared at query time; universe is resolved structurally from each series’ measures. The resolved (anchor, universe) of every series rides in the annotation (Chapter 7.1).
2.6 Broadcast
When a column is brought to a finer anchor than the grain it arrives at, the anchor-action is a broadcast (the prior edition’s fillup):
SELECT (revenue @ {customer}) / (revenue @ {}) AS share_of_total AT {customer}
The denominator revenue @ {} is the Manifold-wide scalar — {} being the defining boundaries collapsed to one point — broadcast unchanged to every customer for the division. Broadcast has a single semantics — replicate: every finer point receives the same coarse value, whether intensive or extensive. Its purpose is to make a coarse value available at a finer grain for a downstream comparison (share-of-total, deviation-from-group-mean) — never to distribute a total into a finer-grained quantity. Proportional down-allocation is not an operation in the language.
The double-count hazard of replicating an extensive value — spreading a total and summing it back up — is foreclosed structurally: a broadcast value carries, in the B-anchor of any reducer that would sum it, the dimension family it was broadcast along, so an attempt to re-aggregate it back across that family is caught by the ordinary B-anchor check (Chapter 5.3) and served, if at all, only with the corresponding critical disclosure. There is no allocation rule to supply and none to get wrong.
2.7 Composite reductions
Two reducers compose when one applies to the result of another, with an explicit intermediate anchor:
SELECT max( sum(revenue @ {transaction}) @ {customer, month} ) AS peak_month AT {customer}
For each customer, the maximum monthly revenue. The inner sum(revenue @ {transaction}) @ {customer, month} produces monthly revenues per customer; the outer max reduces those monthlies to one peak. The intermediate anchor @ {customer, month} is essential — “max revenue per customer” is ambiguous without saying what kind of revenue (daily? monthly? transactional?), and omitting it clarifies input_anchor_ambiguous. A composite reduction is not a separate construct; it is the anchor-ascription rule applied recursively — nested ascriptions, each local, read by the planner as two atoms, the inner feeding the outer (§2.1). Any composite reduction requires an AS alias, since no defensible default name can be derived from a nested expression (Chapter 1.6).
2.8 Subsetting and scans [ROADMAP]
Two further expression forms are documented as the language the envelope grows into by ruling (ADR-035 D1); they are not part of the shipped 0.9.0 envelope, and the ratified grammar spec keeps them roadmap (they are grammar the engine does not yet have). They enter the language when ruled in, not by accretion.
The bracket filter restricts a column reference to a subset of its domain — revenue[region = "east"] — producing a degenerate column that carries its restriction as part of its identity (framework manual, Chapter 6). It differs from WHERE (Chapter 4.1): WHERE restricts the whole statement’s input; a bracket filter restricts an individual reference within one expression. Whether an emptied bucket appears with a zero, an absent value, or not at all is decided by the destination universe’s basis (Chapter 1.5, the B3 law): an events population makes an emptied SUM a true zero; a spine/product population makes it a gap carrying incomplete_data; the resolved basis rides in the annotation.
Scans are order-dependent, anchor-preserving operations — cumsum(revenue @ {customer, day}), lag(...), rolling_mean(..., window=7d) — whose output at each point depends on an order and on neighbors. The order is derived from an orderable axis in the anchor (day) or named explicitly; an order-dependent operation whose order is neither derivable nor named is a clarification. The family-aware parameters reset / within / step lean on the verified dimension-family structure to express calendar and hierarchy intelligence directly (year-to-date via reset = year; same-period-last-year via step = year); each names a coarser level in the order axis’s family, reached by a verified climb, and a parameter riding a CONTRADICTED edge carries that finding. (The full scan reference is Appendix A.)
2.9 The grammar grows by ruling
The envelope is deliberately small, and it grows by ruling (ADR-035 D1), not by accretion: a construct enters the language when it is ruled in, and until then it does not exist. This is why Chapter 6’s examples carry a shipping mark — an executable example runs against the shipped package or it is marked pending/roadmap; the manual is structurally incapable of documenting a query it did not run. What ships in 0.9.0 is the envelope of Chapter 1 with the series forms of §§2.1–2.7: reductions with explicit input pins, maps, composite reductions, the anchor product, one universe per expression, every grain either determined or pinned — and every answer one of the four moods (serve · disclose · clarify · refuse).
Chapter 3. Sugars
The canonical form of Chapter 2 is precise but verbose. Two sugars permit common cases to be written more compactly, with the framework expanding them mechanically to the canonical form before type-checking. Each sugar has a precise applicability condition; outside that condition, the canonical form is required.
3.1 Sugar: default-family reduction implicit
When a column reference appears in a position that requires a reduction to land at the outer expression’s anchor, and the column’s default family’s reducer is the intended reduction, the reducer may be omitted. The column reference col @ a (or just col as an input naming the family) is read as defaultReducer(col) applied appropriately.
In practice this means a writer can usually write revenue instead of sum(revenue) when revenue’s default family is sum:
revenue AT {customer} -- sugared
sum(revenue @ {transaction}) AT {customer} -- canonical (assuming revenue's root is {transaction})
The sugar applies in two combined ways. First, the default reducer is inferred from the column’s declared default family — the framework looks up the column-spec, finds the default family, and uses its reducer. Second, when the column reference has no explicit input anchor and the path from the column’s root to the output anchor is determined (a single fertile path), the input anchor is taken to be the column’s root.
Both inference steps are mechanical and deterministic. The framework refuses to apply the sugar when either inference fails: when the column has no default family (so there is no default reducer), or when the path from root to output anchor is ambiguous (so the input anchor cannot be inferred).
Some specific cases where the sugar does not apply, and the canonical form is required:
-
Mule reductions.
mean,median,weighted_mean, and similar mules are mules precisely because their answer depends on the input grain. The default sugar cannot apply to a mule (the input grain must be explicit), so the canonical form with explicit@ aand the reducer name is required. -
Non-default-family reductions. When the writer wants
maxorminor any reducer other than the column’s default, the reducer must be named. The sugar covers only the default. -
Ambiguous paths. When a column’s root can reach the output anchor by multiple paths (because the dimension lattice has independent hierarchies the path goes through), the framework cannot infer which path is meant. The writer must specify the input anchor explicitly via the canonical form.
3.2 Sugar: omitting @ root(col) for direct fertile reduction
A specific common case deserves separate notice because it is the most-used form. When a column reference appears as an input to a fertile reducer, and the column’s root is the deepest grain involved in the reduction, the input anchor @ <root> may be omitted:
sum(revenue) AT {customer} -- sugared
sum(revenue @ {transaction}) AT {customer} -- canonical, when revenue's root is {transaction}
This is technically a corollary of the path-determinism case of Sugar 3.1, but it is so common that it is worth stating directly. When the writer omits @ a and the framework infers it as the column’s root, that inference is silent and automatic, and the resulting form is what almost all simple queries look like.
3.3 What sugar does not do
Two things the sugar mechanism explicitly does not do, because both would invite ambiguity the framework refuses to resolve silently:
No implicit output anchor. The output anchor (AT A) is always required and never inferred. A query without AT is ill-formed and refused, regardless of what could be guessed. This is the strict rule from Chapter 1.4.
No implicit reducer for non-default families. A bare column name is read as its default family; if the writer wants a non-default reducer, the reducer must be named. The framework does not infer “you probably meant max” from context.
The combination is: the language gives the writer one defensible default (the column’s declared default family with the column’s root as the input anchor, when the path is determined), and otherwise requires explicit specification. The agent and any sugar layer may apply additional defaults as tool-level conveniences, but the formal language does not.
Chapter 4. Clauses
4.1 The WHERE clause: pre-query input filtering
WHERE restricts the input to the query — applied before reductions, narrowing what data the reducers see. Its predicate references dimensions and column values at the input grain (the data being reduced), not the output grain (the result of the reduction).
FROM finance_manifold
SELECT sum(revenue) AT {customer}
WHERE region = "east" AND date >= "2024-01-01"
This computes per-customer revenue over transactions where region is east and date is in 2024 or later. The framework restricts the input data to the predicate’s satisfying rows, then performs the reduction over the restricted input. The output is per-customer sums of east-region 2024+ revenue.
WHERE predicates may reference any dimension or column at the input grain of any series in the query. They are evaluated before reduction, so they see the raw data; references to series expressions (which are output columns) are not permitted in WHERE — those belong in HAVING.
WHERE is a query-time expression-level restriction, not a Manifold-level state change. The Manifold’s columns retain their declared coverage; the query’s result is restricted to what the predicate permits.
When series have different input grains, the predicate applies per series, at each series’ own input grain. A query whose series draw from {transaction} and from {warehouse, day} under WHERE region = "east" restricts each series’ input independently: each predicate dimension must be reachable from that series’ input anchor (directly as a coordinate, or through a verified hierarchy edge — region reached from transaction via transaction → customer → region). If a predicate dimension is not reachable from some series’ input anchor, the query is underdetermined for that series — there is no defensible way to apply the restriction — and the framework asks for clarification rather than silently applying the predicate to some series and not others.
4.2 The HAVING clause: post-query output filtering
HAVING filters the output frame after the series have been computed. Its predicate references the output columns — the series expressions and the anchor dimensions — and is applied after reduction:
FROM finance_manifold
SELECT sum(revenue) AS total_revenue,
count(*) AS transaction_count
AT {customer}
HAVING total_revenue > 10000
This computes per-customer total revenue and transaction count, then keeps only the rows where total_revenue exceeds 10,000. The framework applies the reduction first, then evaluates the HAVING predicate against each row of the output frame.
HAVING references output columns by name — a series’s alias or default name, or an anchor dimension’s name. It cannot reference the input grain (that information has been reduced away by the time HAVING evaluates). The prior edition also permitted referencing a series by repeating its expression; that form is removed — deciding when two expressions are “the same” (before or after desugaring? with or without inferred anchors?) raised more questions than the convenience answered — and the reuse it served is covered cleanly by macro bindings (Chapter 4.5), which expand identically at every site by construction.
The distinction between WHERE and HAVING is precise: WHERE filters input rows (before reduction, referencing input-grain columns); HAVING filters output rows (after reduction, referencing output-grain columns). Some predicates make sense as both — “region = east” can be a WHERE on input rows or a HAVING on an output that includes region — but the meaning differs: WHERE restricts what is reduced; HAVING restricts what is shown.
4.3 The ORDER BY clause
ORDER BY specifies the sort order of the output frame:
ORDER BY total_revenue DESC, customer ASC
Multiple sort columns are evaluated in left-to-right priority. The framework permits sort by any output column — anchor dimensions or series expressions (referenced by alias or expression). ASC is the default direction; DESC reverses.
ORDER BY is also consulted by LIMIT n PER (next section) to determine within-group ordering when partitioned truncation is requested. The same ORDER BY clause serves both purposes: it is the canonical ordering of the result.
4.4 The LIMIT n clause and LIMIT n PER {dims}
LIMIT n keeps the first n rows of the ordered result:
ORDER BY total_revenue DESC
LIMIT 100
This keeps the 100 customers with the highest total revenue — a flat top-N truncation.
LIMIT n PER {dims} keeps the top n rows within each group defined by the named dimensions:
ORDER BY region, total_revenue DESC
LIMIT 5 PER {region}
This keeps the top 5 customers (by total_revenue) within each region — partitioned top-N. The result has 5 rows per region (or fewer if a region has fewer than 5 customers): 5 from the east, 5 from the west, and so on.
The clause has precise requirements:
The PER dimensions must be coordinates of the output anchor. Partitioning by something not in the output frame is meaningless — there is nothing in the result to partition by. The framework refuses PER {region} if region is not part of the query’s output anchor.
The PER dimensions must be a subset of the ORDER BY columns. This makes the within-group order deterministic: the columns in PER group the result, and the remaining ORDER BY columns rank within each group. Without PER ⊆ ORDER BY, the within-group ranking would be unspecified.
Ties are broken by the remaining ORDER BY columns. If 5 customers tie for the 5th-highest revenue within a region, the framework consults the next ORDER BY column to disambiguate. If ORDER BY does not fully disambiguate within PER groups, the framework discloses the under-determination at plan time and asks for clarification at execution time only if the ambiguity actually arises and matters for the truncation.
The empty PER set is permitted and reduces to flat LIMIT n. LIMIT 5 PER {} partitions the result into one group (the whole result) and takes the top 5 — equivalent to plain LIMIT 5. The form is uniform; the empty case is the degenerate flat case.
The LIMIT n PER form is genuinely common in analytics — top products per category, top customers per region, top events per session — and is awkward in conventional SQL. Having it as a first-class clause makes the language match the natural shape of the questions analysts ask.
4.5 The WITH clause: named bindings
The WITH clause declares named bindings used by the statement. There are two kinds, sharing a clause but doing different jobs.
Allocation bindings [Pro] — WITH allocation <name> = <rule> — declare the partition-of-unity weighting an aggregate-across a many-to-many relationship uses (Chapter 5.6, Example 6.12). They are semantic declarations consumed by the planner. (In Core, the undeclared aggregate-across is a clarification, per Chapter 5.6; allocation, which resolves it, is a Pro construct.)
Macro bindings — WITH <name> = <expression> — are a purely syntactic device: name an expression once, reference it by name. Expansion is textual and happens before desugaring and type-checking — each reference is replaced by the bound expression, and then every ordinary rule applies at the site of use. Everything about macros follows from “textual, pre-everything”:
- A macro may be referenced in
SELECTseries,WHERE,HAVING,ORDER BY, and laterWITHbindings. Whether the expanded expression is valid at a site is decided by that clause’s ordinary rules — a post-reduction expression expanded intoWHEREfailsWHERE’s rules exactly as if it had been written there by hand. The macro mechanism grants no new powers; it removes repetition. - Later bindings may reference earlier ones. Recursion, direct or mutual, is refused.
- A series that consists of exactly a bare macro reference takes the macro’s identifier as its default name — the writer has literally named it — so
WITH profit = revenue - cost … SELECT profit AT {customer}needs noAS. Any larger expression involving a macro follows the ordinary naming rules of Chapter 1.6. - Macro names share the namespace of columns, dimensions, and aliases; collisions are refused.
- Macros carry no semantics of their own. The canonical form of a statement is the canonical form of its full expansion, and
EXPLAINshows the expansion — there is no macro in an atom decomposition.
A worked example:
FROM finance_manifold
WITH profit = (revenue - cost)
SELECT profit AT {customer, month}
desugars (assuming co-anchorable defaults) to SELECT (revenue - cost) AS profit AT {customer, month} — and the same profit binding may simultaneously appear in HAVING profit > 0, expanding identically there.
In the shipped envelope’s fixed clause order, WITH precedes SELECT (it binds the names the series then reference); its lexical position carries no scoping meaning beyond that ordering — bindings are visible throughout the statement. (The distinction from AS is worth restating because SQL offers nothing like it: an alias names an output column after reduction and is visible only where output columns exist; a macro names an expression template before everything and is visible wherever expressions are.)
Chapter 5. Type Rules
This chapter specifies the rules the framework applies when type-checking and planning a query. The rules divide into two kinds, and the division matters (Chapter 7 develops it fully). Well-formedness rules are about whether the query is an executable instruction at all — whether it determines one answer. A query failing a well-formedness rule cannot be executed, and the framework responds with a structured clarification naming what is underdetermined. Soundness checks are about the analytical risk of an executable query — B-anchor crossings, coverage gaps, missingness mechanisms, contradicted preconditions. Per the inform-and-serve doctrine, a query that fails a soundness check is not withheld on that account: it is executed, and the risk is returned as a disclosure bound to the result, with severity, quantification, and remedy. The framework withholds a result only when it cannot execute (well-formedness), when the data is not there, or when a declared governance rule forbids it — never on its own analytical judgment.
5.1 Output anchor required
Every query must specify an output anchor via AT. Queries without AT are ill-formed and refused. This is the strict rule from Chapter 1.4.
5.2 Anchor compatibility
The output anchor of every series in a query must be the same (the query’s output anchor). Since series expressions are checked individually and the language requires shared output anchor by structural necessity (Chapter 1.4), this rule is enforced by the AT being a query-level declaration rather than per-series.
For inputs within an expression, anchors must compose correctly:
-
For maps: all operands must be co-anchored. The framework checks that all input column references resolve to the same input anchor. If they do not, the writer must bring them to a common anchor explicitly (by aggregation or broadcast) before mapping. A common anchor is, in full, a common (anchor, universe): columns combined in one expression at a shared anchor are checked for co-universality — same declared universe, verified by reference. A single expression combining measures from different universes is not served with an asymmetry disclosure; it is the
cross_universecategory error (§2c, Chapters 2.5, 7.3), on the query-error channel. Several populations in one view coexist by juxtaposition — separate series, each evaluating in its own universe at a shared anchor — never by combination in one expression. -
For aggregations: the input anchor must be finer than or equal to the output anchor in the relevant dimensions. The framework checks that the input-to-output path is valid (via drops and climbs, per the framework manual’s Chapter 4).
-
For broadcasts: the input anchor must be coarser than the output anchor. Broadcast is replicate-only; no allocation is involved. The result carries the broadcast family in the B-anchor of any summing reducer, so it cannot be silently re-aggregated back along the axis it was spread across (Chapter 2.6).
5.3 B-anchor checking
For every reduction in a query, the framework checks whether the reducer’s B-anchor for the column includes any of the dimension families being eliminated by the reduction:
- A
drop(removing a coordinate from the anchor) crosses the B-anchor when the column’s family-set entry for the reducer lists the dropped axis’s dimension family. - A
climb(replacing a coordinate with a coarser one in the same family) crosses the B-anchor when the family-set entry lists the climbed family.
A B-anchor crossing is a soundness finding, not a well-formedness failure: the query determines an answer (the arithmetic is computable), so per the inform-and-serve doctrine the framework serves the result, bound to a critical disclosure naming the column, the reducer, and the crossed family, quantifying the hazard where it can (the replication or double-count factor), and — when the column’s family set contains an alternative reducer applicable along the crossed axis — naming that alternative as the remedy (typically last for the time axis of a stock). Where a sound route exists that dissolves the crossing rather than flagging it — recomputing from a fertile source at the column’s root instead of re-aggregating a cached image — the planner routes around and discloses the routing as an informational finding instead. A Manifold author may additionally declare a hard stop (WITHHOLD) on a (column, reducer) where governance requires that the crossing never be served; that withholding is the author’s rule, reported as such, not the engine’s analytical judgment.
▸ Frame-QL revision (locus). The B-anchor crossing is detected statically by the planner — the B-anchor and the path’s eliminated families are knowable from the spec, before any data is touched — and it is the planner that attaches the critical disclosure. What is not static is whether a route-around dissolves the crossing; that is a resolution fact, so the column engine may, during execution, recompute from a fertile root and downgrade the finding to an informational route-around (
RECOMPUTED_FROM_DETAIL), or leave itcriticalwhen no sound route exists. The disclosure thus originates at the planner and has its final severity settled at the engine; in every case the number is served. This is the inform-and-serve boundary in miniature: the planner names the risk, the engine never withholds on it.
5.4 Family selection
When a column reference appears with a non-default reducer named, the reducer must be in the column’s family set. Calling a reducer on a column whose family set does not declare it is refused; the framework lists the available reducers in the family set.
When the default-family sugar applies (Chapter 3.1), the framework uses the column’s declared default family’s reducer.
5.5 Order specification for order-dependent operations
Every order-dependent operation (scan, ordered reducer) must have its order determinable:
- The order may be derived from the input anchor when the anchor contains an axis with a natural order (typically a temporal axis).
- The order may be specified explicitly in the operation’s syntax.
If neither applies — the operation is order-dependent but no order is determined — the framework refuses, naming the operation and the missing order specification.
For the family-aware scan parameters (reset, within, step — Chapter 2.8): the named level must be a member of the same dimension family as the scan’s order axis, reachable from the order’s grain by declared roll-up edges. A level outside the order’s family, or unreachable from its grain, leaves the parameter underdetermined — a clarification. The climb the parameter rides is checked like any climb: its edge’s integrity verdict enters the query’s dependency cone, and a CONTRADICTED edge surfaces as a disclosure on the served result.
5.6 Many-to-many
Aggregating a measure across a many-to-many relationship has a precondition the relationship does not supply — functionality, the partition property that makes per-bucket totals reconcile to the grand total. The check applies specifically to aggregate-across operations; queries that use the many-to-many in other ways (filtering, listing memberships, querying either side independently) invoke no such precondition and are unaffected.
When a query aggregates across a many-to-many without a declared resolution, the framework serves the membership aggregation — each fine entity contributing its full value to every bucket it belongs to — bound to a disclosure stating that the precondition is unmet: per-bucket totals overlap, their sum exceeds the grand total by the overlap amount (quantified in the finding), and the result answers a membership question (“revenue of products in each category”), not a partition question. The disclosure names the three resolutions as remedies: a membership filter (accept the overlap deliberately), a primary designation (convert the relationship to functional), or an allocation declared via WITH allocation (Example 6.12), which supplies the partition-of-unity that makes totals reconcile.
A query that declares one of the three resolutions is computed under it with no precondition finding.
▸ Frame-QL revision (column-foundation). The paragraph above describes a join-based engine, where the membership aggregation is always computable and so can be served with an overlap disclosure. Frame-QL’s engine (ADR-031) transports a measure along functional edges and never joins; a non-functional (M:N) edge is not a traversable path, so the membership aggregation is not expressible at all without one of the three resolutions. The undeclared aggregate-across is therefore caught by the planner as a well-formedness clarification — “this aggregate-across rides a non-functional edge and is underdetermined; supply a membership filter, a primary designation, or
WITH allocation” — returned statically, before execution, with no result. The three remedies are unchanged; only the outcome class moves, from serve-with-disclosure to clarify, because the engine cannot fabricate a number it has no path to. A declared resolution makes the query well-formed exactly as before. (This is the one place the inform-and-serve doctrine yields to well-formedness: the engine still never withholds a number it can produce — here it genuinely cannot.)
▸ Frame-QL revision (RELATE faces, v0.11). The clarification above is now a menu. A relationship may declare faces — named crossing dispositions — and the undeclared-aggregate-across clarify lists the declared faces with their folklore as its alternatives, so the human re-issues by naming one. A face names the value’s disposition on the trip, never the selection criterion; the three are a self-teaching verb triad, mutually exclusive and jointly covering the sensible crossings of a many-to-many by an additive quantity:
touch— the value reaches every match. Per-bucket totals overlap and their sum exceeds the grand total (and, where the fine side has members in no bucket, can also fall short of it — both skews disclosed). This is the membership question (“revenue of products in each category”), now executed, not just described.assign— the value goes to exactly one match (a declared canonical pick — by a primary flag, first-listed, max-weight — adjudicated as “exactly one per fine entity” regardless of the criterion). Collapses the M:N into a functional edge.alloc— the value splits across matches (equal shares, or a declared weight); per-bucket totals reconcile to the grand total. TheWITH allocationconstruct of §6.12 is this face’s parameter.A face is declared on the relationship (not parameterized per query —
EXPLAINstays static and the cache keys the name), described per the folklore rule, and adjudicated at publish: a face is closed by default, and its license opens the crossing. Faces are addressed as a qualified coordinate,{<coordinate>.<face>}:RELATE product <-> category VIA product_categories(product_id, category_id) FACES { touch = TOUCH -- "revenue reaches every category a product sits in — deliberately multi-counted; totals exceed the grand total" } NOTE "a product belongs to up to 3 categories"SELECT revenue AT {category.touch} -- disclose: over_count (caution) + coverage (info)The bare
{category}stays uniformly barred and now clarifies with the face menu;{category.touch}executes the join-multiply and serves in disclose, carrying the over-count as a material caveat and the coverage (the covered/uncovered count) as an informational one. Naming honesty holds:category.touch≠category, marked in the grain name. Per-basis licensing, v1 = events only — on an events population the expansion is honest arithmetic; on a spine/grid, replication would corrupt the grid’s own completeness claim, so the crossing is refused there until that thinking lands.v1 executes
touchonly.assignandallocare declared-but-deferred: the grammar knows them, but declaring one is an honest, fail-closed parse refusal — “FACE<name>: schemeassignis declared-but-deferred — v1 executes TOUCH only (assign/alloc are post-launch ledger items)” — so no manifold can carry an inert face that silently serves nothing.
5.7 Coverage compatibility [Pro]
When a query combines columns at a shared anchor, the framework consults the coverage states of the columns:
- Complete columns combine freely. No coverage finding is produced.
- Filtered columns combine over the intersection of their restrictions. The result inherits the intersection as its coverage; the result’s lineage records the intersection, and the annotation states the universe the answer reflects. If the restrictions are disjoint, no data supports the combination — the framework returns no result and refuses: this is a data-gap outcome, not an analytical judgment.
- Filtered combined with complete produces filtered. The complete column is implicitly restricted to the filtered column’s domain for the combination; the result inherits the filtered column’s restriction, disclosed in the annotation.
- Sampling restrictions combine differently from predicate restrictions, and the framework says so. Two columns each filtered by an independent random sample intersect, in expectation, on the product of their fractions — two independent 1% samples share ~0.01% of the universe — which is rarely the analysis anyone intends. The combination is served over the realized intersection with a critical disclosure quantifying the surviving fraction, unless the samples are declared coordinated (drawn over the same units), in which case they combine like any shared restriction.
- Quarantined columns do not participate in cross-column combination. A quarantined column is one whose incompleteness the author has not declared usable — its exclusion is the Manifold author’s standing coverage governance, and the framework reports it as such: the query returns no result for the affected combination, refusing it because the column is quarantined and naming the remediation (declare the coverage with its restriction, or address the defect at the source). This withholding belongs to the author’s governance, not to the engine’s analytical judgment.
Coverage in the marginal-mismatch range (between the Manifold’s configured thresholds) is served with the coverage state disclosed; combinations across a coverage gap carry the gap, its asymmetry, and the resulting bias direction in the annotation. The coverage machinery is part of the framework’s universe-integrity layer (per the framework manual’s Chapter 10), and its findings carry the certificate state of the preconditions they derive from.
Chapter 6. Examples
This chapter shows worked examples that exercise the language across its common shapes. Each example shows the query, the canonical desugared form where it differs, and a brief note on what the query computes.
6.1 Simple aggregation
FROM finance_manifold
SELECT revenue AT {customer}
Canonical: FROM finance_manifold SELECT sum(revenue @ {transaction}) AT {customer} (assuming sum is revenue’s default family and {transaction} is revenue’s root).
Computes per-customer revenue total.
6.2 Multiple metrics at a shared anchor
FROM finance_manifold
SELECT revenue,
cost,
count(*) AS transaction_count
AT {customer, month}
Computes per-customer-month revenue, cost, and transaction count. All three series share the output anchor {customer, month}.
6.3 Composite reduction with explicit intermediate anchor
FROM finance_manifold
SELECT max( sum(revenue @ {transaction}) @ {customer, month} ) AS max_monthly_revenue
AT {customer}
Computes, for each customer, the maximum monthly revenue. The intermediate anchor @ {customer, month} is required — it specifies that the question is about monthly maxima, not daily or transaction-level maxima. Without the inner anchor, the question would be ambiguous and the framework would ask for the grain (a clarification, per Chapter 7). The AS alias is also required for any composite reduction, since the framework cannot derive a default name from a nested expression.
6.4 Mean with explicit input anchor
FROM product_manifold
SELECT mean( engagement_score @ {customer, week} ) AT {customer}
Computes the per-customer mean of weekly engagement scores. The mule reducer mean requires the input anchor to be explicit — “average engagement per customer” is meaningless without saying what kind of engagement is being averaged.
6.5 Map of co-anchored columns
FROM finance_manifold
SELECT (revenue - cost) AS profit AT {customer, day}
Computes per-customer-day profit by subtracting cost from revenue at the daily grain. Revenue and cost must be co-anchorable at {customer, day}; the map combines them point by point.
6.6 Ratio across grains
FROM finance_manifold
SELECT ( revenue @ {customer} ) / ( revenue @ {} ) AS share_of_total
AT {customer}
Computes each customer’s share of total revenue. The numerator is per-customer revenue; the denominator is the Manifold-wide grand total ({} being the Manifold’s defining boundaries collapsed to one point). The denominator is broadcast — replicated unchanged — to every customer for the division.
6.7 Bracket filter on a column
FROM finance_manifold
SELECT revenue[region = "east"] AS east_revenue,
revenue AS total_revenue
AT {customer}
Computes per-customer east-region revenue alongside total revenue. The bracket filter restricts the revenue column to its east-region subset; the result column appears alongside unrestricted revenue.
6.8 WHERE: pre-query filtering
FROM finance_manifold
SELECT revenue AT {customer}
WHERE date >= "2024-01-01" AND region IN ("east", "west")
Restricts the input to transactions in 2024+ and east or west regions before computing per-customer revenue. The output is per-customer revenue over the restricted input.
6.9 HAVING: post-query filtering
FROM finance_manifold
SELECT revenue AS total_revenue AT {customer}
HAVING total_revenue > 50000
Computes per-customer revenue then filters out customers with revenue at or below 50,000.
6.10 Top N per group
FROM product_manifold
SELECT product_revenue AT {category, product}
ORDER BY category, product_revenue DESC
LIMIT 5 PER {category}
Computes per-category-product revenue, sorts by category then by revenue descending, then keeps the top 5 products per category. The result has up to 5 rows per category, with each category’s rows ordered by revenue.
6.11 Scan for running total
FROM finance_manifold
SELECT cumsum( revenue @ {customer, day} ) AS revenue_to_date
AT {customer, day}
Computes the running cumulative revenue per customer, ordered by day. The scan partitions by customer and accumulates by day; the result is co-anchored with the input.
6.12 Many-to-many with allocation [ROADMAP]
The WITH allocation <name> = … form is a grow-by-ruling surface (ADR-035 D1): it is not part of the
shipped envelope grammar, so the block below is marked frameql-illformed — the shipped parser rejects
its WITH allocation head, and the manual’s self-check asserts exactly that until the form ships.
FROM product_manifold
WITH allocation product_to_category = proportional_to(category_weight)
SELECT sum( revenue @ {product} ) AT {category}
Computes per-category revenue when products belong to multiple categories. The WITH allocation clause declares the allocation rule the framework uses to apportion each product’s revenue across its categories; without this declaration, the aggregation across the many-to-many relationship would be served with membership semantics and an overlap disclosure (Chapter 5.6 — or, under the column-foundation, raised as a clarification; see the revision note there); the allocation supplies the partition-of-unity that makes per-category totals reconcile, with no precondition finding.
6.13 Time intelligence: year-to-date and year-over-year
FROM finance_manifold
SELECT cumsum( revenue @ {customer, day}, reset = year ) AS revenue_ytd
AT {customer, day}
Computes per-customer year-to-date revenue: the running sum restarts at each year boundary, the year level reached from day through the verified time family.
FROM finance_manifold
SELECT ( revenue - lag(revenue, 1, step = year) ) / lag(revenue, 1, step = year) AS yoy_growth
AT {customer, month}
Computes per-customer year-over-year revenue growth at month grain: lag(…, 1, step = year) is the same month one year earlier, and the growth is an ordinary map over the co-anchored pair.
6.14 Macro bindings for reuse
FROM finance_manifold
WITH profit = (revenue - cost)
SELECT profit AT {customer, month}
HAVING profit > 0
ORDER BY profit DESC
LIMIT 10
The profit macro expands textually at every reference site before type-checking; as a bare-macro series it takes profit as its default name. The statement desugars to one canonical query with (revenue - cost) AS profit selected, filtered, and sorted — visible in full via EXPLAIN.
6.15 The envelope, end to end
The top-3 stores per region by gross, with each region’s typical order value — the ratified acceptance target for the envelope (ADR-035), exercising anchor products, an auto-leading coarser coordinate, a WITH macro, two different input grains juxtaposed in one frame, and PER ⊆ ORDER BY:
FROM retail
WITH line = revenue @ {transaction}
SELECT sum(line) AS gross,
avg(aov @ {day}) AS typical
AT { region * store }
ORDER BY region, gross DESC
LIMIT 3 PER { region }
Reading it clause by clause: AT {region * store} stands at the (region, store) grid — region auto-leads as the coarser coordinate (the edge region ← store carries it), an anchor coordinate rather than a selected series, and so nameable by ORDER BY/PER. WITH line = revenue @ {transaction} binds the transaction-grain line once; sum(line) reduces it to (region, store) — the reduction is edge-carried (transaction → store → region), the grammar admitting it while the kernel’s edge verdict decides serve vs disclose. typical = avg(aov @ {day}) reads aov at day grain and averages to (region, store) — two different input grains (transaction, day) collapsing into one frame, legal because each series names its own @. ORDER BY region, gross DESC references the frame’s own columns; region leads so groups are contiguous, gross DESC ranks within. LIMIT 3 PER {region} keeps the top 3 per region — well-posed because region is both an anchor coordinate AND an ORDER BY key (the conjoined PER law: PER keys are anchor coordinates only, and PER ⊆ ORDER BY). PER {typical} would refuse (an alias, not a coordinate); PER {store} would refuse unless store is added to ORDER BY.
Chapter 7. Outcomes, Disclosures, and Clarifications
This chapter specifies what a query returns and what it returns it with. The prior edition specified a refusal taxonomy; this edition replaces it, per the inform-and-serve doctrine: analytical risk is disclosed, never used as grounds to withhold a result the query determines. The chapter specifies the universal return contract, the complete outcome taxonomy, the structure of disclosures, and the control model that governs who may cause a result to be withheld.
7.1 The universal return contract: (result, annotation)
Every query returns a pair:
result— the output frame. It is null in exactly the withholding outcomes of 7.3 (clarify, no-data, governance/authorization), and present in every other case, including every analytically risky case.annotation— always present, on every path. On the clean path it is a single no-riskOKfinding. On served-with-risk paths it is the result’s risk profile. On withholding paths it carries the why and the remedy.
An annotation is a list of targeted findings plus a frame-level severity rollup. Each finding carries: a code (the machine-readable category); a severity (none < info < caution < critical); the originating four-state certificate_state (VERIFIED / CORROBORATED / CONTRADICTED / UNTESTABLE); a targeted subject (the specific metric, column, anchor, or cell it concerns — never smeared frame-wide); a plain-language headline and detail; a quantification where one is computable (a caveat with a number is honest; “may be inaccurate” is noise); the bias direction and bound where determinable; an actionable remedy; a requires_acknowledgment flag; and a provenance_ref linking the finding to the certificate entry or precondition it derives from, so the finding travels bound to the frame and remains auditable.
Beyond its findings, the annotation always carries the canonical desugared form of the statement as executed — every macro expanded, every sugar normalized, every inferred anchor and reducer explicit, the FROM clause and its resolved Manifold identity (Manifold, version, refresh, certificate state) included, and the resolved (anchor, universe) of every series — together with a reference to the plan’s atom decomposition (Chapter 2.1). Every result is thereby auditable against the exact reading the framework gave the query: a human checks the interpretation at a glance, and an agent learns the canonical language from every exchange rather than only from documentation. (EXPLAIN, Chapter 1.7, returns the same material without execution.)
The annotation is computed in full for every query. A Manifold’s disclosure_default (with optional per-call override) controls how much of it is returned — MINIMAL / STANDARD / FULL — as a serialization-time view, never as a control on what is assessed, and subject to a non-negotiable safety floor: the overall severity, every critical finding, and every requires_acknowledgment flag are present at every level. Verbosity may trim noise; it may never silence risk. The canonical form rides at every disclosure level — interpretation is never noise.
7.2 The outcome taxonomy
Four outcomes exhaust the contract — the shipped wire’s four moods, serve · disclose · clarify · refuse (the cross_universe category error rides the separate query-error channel, not a mood):
Serve, clean. Every precondition in the query’s dependency cone is VERIFIED or CORROBORATED, no soundness finding fires. The annotation is the single OK finding.
Serve, with disclosures. The query is executable and determinate, and one or more soundness findings apply. The result is served; the findings ride with it. This outcome has a first-class sub-case, route-around: where the planner can dissolve a risk rather than flag it — recomputing a mule from its fertile sources instead of re-aggregating a cached image; serving from transaction detail when a CONTRADICTED or stale pre-aggregate would otherwise be used — it does so, and discloses the routing as an info finding (RECOMPUTED_FROM_DETAIL, SERVED_FROM_DETAIL) rather than disclosing a risk that no longer exists. Only when no sound route exists does the risk surface as a caution or critical finding on the served number.
Clarify. The query is not an executable instruction: it does not determine one answer. The framework returns no result and opens a structured clarification naming exactly what is underdetermined and what would determine it. How the clarification is conducted is a surface decision — a console may show the message, an agent may ask the question in the requester’s vocabulary, an MCP client may receive the structured payload and resubmit.
Refuse. The query is well-formed but no result can or may be produced for a non-analytical reason: the data is not there, or a declared governance rule withholds it. The framework returns no result and states the reason and the remedy. (▸ shipped-law: this is the wire’s fourth mood, refuse. The Third-Edition lineage called it inform, under ADR-020’s “inform-and-serve” doctrine — same outcome, same spirit; the shipped name is refuse.)
There is no fifth outcome. In particular there is no analytical-risk withholding: any result that is determinate and producible for an entitled caller is produced, with its risk on its face.
▸ Frame-QL revision (the doctrine’s locus). The four outcomes map cleanly onto the two-projection architecture (Chapter 2.1). Clarify and the governance/no-data half of refuse are planner outcomes: they are decided statically, from vocabulary and shape, before the column engine resolves anything — ambiguity, a missing anchor, an unknown operator, a type mismatch, a non-traversable edge, a
WITHHOLDor access rule, an empty realized support. Serve, clean and serve, with disclosures are engine outcomes: once a query is well-formed and permitted, the engine resolves it and any surviving analytical risk rides out as a disclosure. The boundary between “may withhold” and “must serve” is therefore the boundary between the planner and the engine — the planner may refuse what is not an executable, permitted instruction; the engine, having an executable one, never withholds on its own analytical judgment. This is why the canonical form is both the type-checked meaning and the unit of planning: the same decomposition that the planner validates is the decomposition the engine serves.
7.3 The withholding outcomes, enumerated
A query yields no result in exactly these cases:
Clarifications — the query cannot be executed as posed:
- Missing output anchor. No
ATclause. What to clarify: the output anchor. - Missing required alias. A series with no defensible default name (composite reduction, map expression, bracket filter, conditional) and no
AS. What to clarify: the name. - Name collision. Two series whose default or declared names collide, or a series colliding with an anchor dimension’s name. What to clarify: a disambiguating alias.
- Anchor incompatibility. Operands of a map that do not resolve to a common input anchor, with no expressed path to one. What to clarify: the explicit aggregation or broadcast that brings them together, or a restructuring.
- Mule without explicit input anchor (shipped reason:
input_anchor_ambiguous; the multi-input case isco_anchor_ambiguous). A reduction whose answer depends on an input grain that has not been named. What to clarify: the input pin (Chapter 2.3). (▸ shipped-law reconciliation, 2026-07-17: the input anchor is now written inline —avg(aov@day) @ month— not as a separate@ {anchor}clause.) - Ambiguous path. A column whose root reaches the output anchor by more than one hierarchy path (sibling hierarchies), unnamed. What to clarify: which family is climbed.
- Cross-universe expression (shipped reason:
cross_universe, on the query-error channel — NOT a clarify). A single column expression that combines measures from more than one universe. This is a category error, not a hedged answer: an expression evaluates in exactly one universe (the §2c expression law, Chapter 2.5). What to do: ask each population separately, or reconcile them — and to place several populations in one view, juxtapose them (separate series, each in its own universe). (▸ shipped-law reconciliation, 2026-07-17: net-new — the retired cross-universe “wedge” lands here.) - Order missing. An order-dependent operation (scan, ordered reducer) whose order is neither derivable from the anchor nor specified. What to clarify: the order.
- Family not declared. A reducer named on a column whose family set does not declare it. What to clarify: a declared reducer (the clarification lists the family set), or an extension of the column-spec.
- Unknown operator. An operator name that resolves to no reducer, scan, or map function in the Manifold’s operator registry (distinct from family not declared, which is a known reducer absent from this column’s family set). What to clarify: a registered operator, or an extension of the registry. (▸ Frame-QL revision: made explicit; the planner typechecks operator names against the registry before resolution.)
- Type mismatch. An operator applied to an input whose type its signature does not accept (e.g. a numeric reducer on a String column). What to clarify: a cast, or a type-appropriate operator. (▸ Frame-QL revision: made explicit; signatures are checked at the planner.)
- Non-traversable edge (fan-out) (shipped reason:
non_functional_transport). An aggregate-across that rides a non-functional (M:N) relationship without a declared resolution (Chapter 5.6). What to clarify: a membership filter, a primary designation, orWITH allocation. (▸ Frame-QL revision: under the column-foundation this is a clarification, not a served membership aggregation.) - Unreachable predicate dimension (shipped reason:
filter_unreachable). AWHEREdimension not reachable from some series’ input anchor (Chapter 4.1). (▸ shipped-law reconciliation, 2026-07-17: the shipped envelope planner raisesfilter_unreachablefor aWHEREpredicate whose dimension no series’ input grain can reach.)
Refusals — the query executes against nothing, or a rule withholds it:
- No data / gap (shipped reason:
incomplete_data/data_gap). The rows are not there: an empty realized anchor, disjoint filtered restrictions, a requested combination the bound data cannot support — or, in a spine/product universe, a gap in the expected grid (per the basis law, Chapter 1.5). The figure covers what is present; the gap is disclosed, never a silent zero. - Quarantined column. A cross-column combination touching a column whose incompleteness the author has not declared usable (Chapter 5.7) — the author’s standing coverage governance.
- Author hard stop. A
(column, reducer)the Manifold author has declaredWITHHOLD(see 7.5): a governance rule, reported as the author’s rule with the declared rationale and the alternative families. - Authorization. The caller is not entitled to the data under the Manifold’s access rules. (The access-rule layer is specified separately; the outcome category is reserved here.)
Everything formerly in this chapter’s refusal taxonomy that does not appear above — B-anchor violations, mule re-aggregation, many-to-many aggregate-across, coverage mismatch, MNAR, contradicted integrity preconditions — has moved to 7.4: those queries are served, with disclosures.
7.4 The disclosure catalog
The principal soundness findings, each carrying the fields of 7.1:
- B-anchor crossing (
critical; shipped code:b_anchor_crossing/blocked_reduction). A reduction coarsening across a blocked family — summing a stock over time (level.sum @ store*cal.month), re-summing a broadcast value along its broadcast axis. Served with the crossing named, the double-count or replication factor quantified, and the applicable alternative reducer as remedy (.lastfor a stock over time; Chapter 5.3). - Mule re-aggregation (route-around,
info;cautionif no route). An attempt to re-aggregate a sterile result is recomputed from the fertile sources at root where they exist (the average is rebuilt from sum and count —RECOMPUTED_FROM_DETAIL); where no fertile decomposition exists (exact distinct count, exact median, mode), the result is recomputed from base at the output grain — the grain at which it is computable — and the finding states it cannot be soundly re-aggregated past that grain, with the sketch-backed approximate family (approx_distinct,approx_quantile) as the reaggregable remedy. - Many-to-many membership aggregation (
caution/criticalby overlap). Aggregate-across without a declared resolution, served with membership semantics and the overlap quantified (Chapter 5.6); remedies: filter, primary designation,WITH allocation. (▸ Frame-QL revision: this served-disclosure applies to a join-capable engine; under the column-foundation the undeclared case is a planner clarification, not a served result — see the revision note in Chapter 5.6.) - Contradicted precondition in the dependency cone (route-around where possible; otherwise
caution/critical). A broken hierarchy edge, a failed co-anchoring, a coverage drift: where a route that avoids the contradicted condition exists, the planner takes it and discloses the routing; otherwise the result is served carrying the finding with the contradiction’s specifics (“customer 19847 maps to both ‘east’ and ‘west’”) and itscertificate_state = CONTRADICTED. - MNAR exclusion [Pro] (
caution/critical,certificate_state = UNTESTABLE). A reduction over a column whose missingness is declared value-dependent: no correction the observed data supports exists; the result is served as the observed-data figure with the unknown-direction bias stated, never presented as a population estimate. - Rescaling applied [Pro] (
info/caution). An extensive reduction under MCAR has been skip-and-rescaled to an unbiased population-total estimate; the finding carries the rescale factor (N_total / N_observed) so the result can be reconciled against a raw source-system sum. - Coverage [Pro] (
info→criticalby gap). Restricted universes, marginal overlaps, asymmetric gaps with bias direction, independent-sample intersections (Chapter 5.7). - Staleness (
info/caution). The result reflects a data version whose refresh timestamps the finding carries.
The disclosure severities are findings about the data and the operation; they are never the engine’s grounds for withholding. The safety asymmetry of the certificate is preserved verbatim in the findings: a finding’s certificate_state retains the state a pre-ADR-020 refusal would have minted — CONTRADICTED or UNTESTABLE — demotable at most to CORROBORATED by a passing data check, never silently to VERIFIED.
7.5 The four-party control model
Withholding and handling are governed by four parties, each with a distinct, legitimate authority. The engine’s neutrality among them is the design’s load-bearing property.
The Manifold author exercises corporate governance. The author already controls the Manifold’s entire shape — scope, sources, columns, operators, the V/M/B declarations — and the same authority extends to declared withholding rules: per-(column, reducer) hard stops (WITHHOLD in the definition language), access and entitlement rules, and policy customization of operator behavior. When an author rule fires, the engine reports it as the author’s rule: the refuse outcome names the declaration, its rationale where given, and the permitted alternatives.
The engine withholds only what it cannot do: an ambiguous query is not an executable instruction and must be clarified first; an empty data support cannot be served. These are the engine’s only inherent non-serves — soft, structural, and free of judgment. In every other case the engine’s stance is fixed: serve, and disclose. The engine executes governance rules it did not author and produces disclosures it does not act on; it never withholds on its own analytical opinion.
The surface — a query console, the agent, an MCP server, an embedding application — decides how outcomes are handled: how a clarification dialogue is conducted, how a refuse is presented, and what to do with a served result given its disclosure content. Severity-driven presentation gates (blur-until-acknowledged on requires_acknowledgment, confirmation steps on critical findings) are surface decisions, applied to presentation, never API-level withholding — entitled programmatic callers receive the bytes with the annotation, or composition breaks.
The user may declare their own suppression preferences — “do not return results whose disclosures match these conditions” — implemented at the surface, or communicated to the engine as a per-call policy. This is the user governing their own consumption, the symmetric counterpart of the author governing the corpus.
One sentence holds the model together: the author may forbid, the engine may only clarify or report emptiness, the surface decides presentation, and the user decides their own threshold — and every one of these is visible in the annotation, so no withholding and no risk is ever silent.
7.6 Message conventions
Every clarification, refuse, and finding includes: its category code; the specific column, expression, or precondition concerned; the data version and certificate state at the time; and a remediation where one is determinable. The conventions hold across surfaces because they are properties of the structured payload, not of any one presentation: a clarification like “the inner grain of this mean is underdetermined — name the input anchor (@ {customer, day} or @ {customer, transaction} give different answers)” and a finding like “the roll-up edge customer → region is CONTRADICTED (customer 19847 maps to both ‘east’ and ‘west’); served from transaction detail, routing disclosed” are designed to be acted on — by a person reading them or by an agent in a propose-validate-refine loop.
Chapter 8. Sugar and Extensions
8.1 The role of sugar
Frame-QL is a small language: a query has a fixed clause structure, expressions follow the canonical form, and the sugars of Chapter 3 cover the common-case shortcuts. The agent and various surfaces may apply additional sugar layers — domain-specific shortcuts, natural-language-to-query expansion, embedded query fragments in other languages — that desugar to Frame-QL before reaching the framework.
This is by design. The formal language stays small and analyzable; surface sugars handle ergonomics. The framework type-checks the desugared Frame-QL, so any sugar that produces well-formed Frame-QL is admissible; sugar that produces ill-formed Frame-QL is rejected at the framework boundary, no matter how natural it looked at the surface.
8.2 Name aliases
A Manifold may declare operator aliases through its operator registry — total for sum, unique_visitors for count_distinct(visitor_id), etc. — that are valid in queries against that Manifold:
FROM retail_manifold
SELECT total(revenue), unique_visitors AT {store}
The aliases are Manifold-level (declared in the operator registry), not universal. The same alias may mean different things in different Manifolds, or be undeclared in some; the framework resolves aliases against the named Manifold’s registry.
8.3 Recommended sugar patterns at the surface
Surfaces building on Frame-QL may apply patterns like:
- Implicit
AT {}for ad-hoc exploration queries without explicit output anchor (with the desugared form shown to the user for transparency). - Implicit
FROM <manifold_name>when the active Manifold is unambiguous in the surface’s context — the sugar Chapter 1.3 specifies in full. The formal language requiresFROM; the surface injects it before submission, and the injection is licensed by disclosure: the annotation’s canonical form shows the statement as executed,FROMand resolved version included, on every round trip. Injection is ergonomics, never authorization (Chapter 1.3). - Natural-language-to-canonical-form translation by the agent.
- Domain-vocabulary substitution (the team’s terms mapped to the underlying column and reducer names).
- A pipeline rendering of the canonical form for surfaces whose users prefer left-to-right reading of deep compositions —
revenue | sum @ {customer, month} | max AT {customer}— where each stage desugars mechanically to one nesting level of the canonical form (max( sum(revenue) @ {customer, month} ) AT {customer}). The pipeline is presentation, not semantics: the framework receives, type-checks, and explains only the canonical form.
The framework places no constraints on what surfaces do, as long as what they hand to the framework is well-formed Frame-QL.
Appendix A: Operator Reference
The registry is one umbrella, and it is the planner’s contract with the engine. Reducers, scans, and map functions are three kinds of operator in one installation-level registry. The registry holds, for each operator, its name, its kind, and its type signature; the planner reads exactly this to typecheck an operator against its inputs and to route it — a reducer is decomposed into an atom
op(input @ a_in) @ a_out, a scan is handed to the column engine to run against a derived order, a map is evaluated by the planner over co-anchored results. The mechanics — howsumcombines, how an HLL sketch merges, howrolling_meanwalks a window — live in the engine, never in the registry. This is the same logical/physical split the type system uses: the registry is vocabulary (names, kinds, signatures, fertility, the{mechanism → behavior}map), the engine is implementation. Custom operators and custom datatypes (e.g. anHLLSketch-typed column with anapprox_distinctfertile reducer) are added here, at the registry, which is why a new operator needs no change to the planner’s machinery — only a new registry entry the planner can read. [Pro] custom operators/types and the sketch reducers below beyond a basic set.
Reducers
| Reducer | Type | Notes |
|---|---|---|
sum | fertile | extensive monoid; carrier is the value |
count, count(*) | fertile | counts present (non-missing) values |
max, min | fertile | idempotent monoids; overlap-robust |
product | fertile | watch for overflow / zero |
any, all | fertile | logical monoids on Boolean |
mean | mule | computed at presentation from sum and count |
weighted_mean | mule | computed from weighted sum and weight sum |
variance, stddev | mule | computed from count, sum, sum-of-squares |
median (exact) | mule | no fertile carrier; recomputed from base at the output grain, not re-aggregable past it |
mode | mule | no fertile carrier; recomputed from base at the output grain, not re-aggregable past it |
approx_distinct | fertile via HLL | HLL sketch is fertile; estimate at presentation. [Pro] beyond Core’s basic HLL |
approx_quantile | fertile via t-digest | t-digest sketch is fertile. [Pro] |
approx_frequency | fertile via count-min | count-min sketch is fertile. [Pro] |
last | ordered, fertile-along-its-order | requires an order; path-invariant along the order |
first | ordered, fertile-along-its-order | requires an order; path-invariant along the order |
value_at_max, value_at_min | ordered, mule | needs the full set |
A note on last and first as family founders: an ordered reducer may appear in a column’s family set (a stock measure’s LAST family is the canonical case — the closing balance over time). Because order is never part of a cache key, what such a family caches is the un-ordered slice of its source; the ordered reduction is applied (or recomputed) at serve time against the declared order. The family is path-invariant along its order — last-of-lasts is the global last — which is exactly the property the serve-time application relies on.
Map functions
Arithmetic: +, -, *, /, %, unary -.
Comparison: =, !=, <, <=, >, >=, between, in.
Logical: and, or, not.
Conditional: if(predicate, then, else), case ... when ... then ... else ... end.
Null/missing: is_null, is_missing, coalesce.
Numeric: log, exp, sqrt, abs, sign, ceil, floor, round.
String: concat, substring, lower, upper, trim, length.
Temporal: year, month, day, week, quarter, date_diff, date_add.
Scan functions
cumsum, cumprod, cummin, cummax — running aggregates (prefix of the fertile reducer).
rolling_sum, rolling_mean, rolling_min, rolling_max, rolling_count — windowed (require a window).
lag(col, n), lead(col, n) — shifted values.
rank, dense_rank, row_number — ordinal positions.
pct_change — relative change from previous.
ewm_mean — exponentially-weighted mean.
Scan parameters, passed by keyword: window (rolling extent), n (shift offset), by (explicit order column), and the family-aware trio reset / within / step (Chapter 2.8), each naming a coarser level in the order axis’s dimension family.
Type predicates and casts
is_<type> — predicate for value type (e.g., is_integer, is_string).
cast(col, <type>) — explicit type conversion.
Appendix B: Reserved Keywords
The shipped envelope statement keywords (columna-core 0.9.0; case-insensitive, whole-word): EXPLAIN, FROM, WITH, SELECT, AS, AT, WHERE, HAVING, ORDER, BY, LIMIT, PER, ASC, DESC, AND. These are the clauses of the envelope (Chapter 1.2), in fixed order; SELECT and AT are required, the rest optional (FROM defaults to the bound Manifold).
Structural markers. @ {…} — the input anchor, on a column reference or reduction, the input-anchor marker universally; AT {…} — the output grain, the sole output-anchor declaration; * — the anchor product (store * cal.month), the same operator as UNIVERSE u = store * day (comma accepted on input, * canonical); {…} — an anchor set (a product of levels). The trailing-@ output form is retired — a top-level @ no longer spells an output anchor (Appendix D).
ON is a DEFINITION-language clause, not a query keyword. A population is pinned in definitions; the §2c universe law resolves universe structurally at query time (Chapters 1.5, 2.5). An ON in a query is a syntax error.
Grow-by-ruling keywords — documented, not yet in the shipped envelope statement grammar (they enter by ruling, ADR-035 D1): VERSION (the FROM … VERSION n pin, Chapter 1.3, [SCHEDULED]); the scan-parameter keywords window, n, by, reset, within, step (recognized in scan-argument position, Chapter 2.8 [ROADMAP]); the bracket-filter [...] syntax on column references (Chapter 2.8 [ROADMAP]); and the expression-level OR, NOT, IF, CASE, WHEN, THEN, ELSE, END, BETWEEN, IN (series-internal expression text is captured verbatim and delegated to the expression parser at plan time, so its dialect grows independently of the envelope). WITH allocation is [Pro] (Chapter 4.5).
Reserved for future use: INHERIT, JOIN, COMPOSE, ASSERT, ACCESS.
Operator names from Appendix A are reserved when used in operator position; the same identifiers may be used as column names in Manifolds that declare them.
▸ shipped-law reconciliation (2026-07-17), RESOLVED. The prior edition of this appendix flagged
FROM/SELECT/AT/{…}as “Coframe canonical form, not the shipped grammar,” pending the Coframe→envelope rewrite. That rewrite has landed: the envelopeSELECT … AT {…}is the shipped grammar (Chapter 1), so these are the shipped query keywords, reconciled above. The retired terse@-fragment (its:label and trailing-@output) moves to Appendix D.
Appendix C: For SQL Speakers — Five Intuitions That Transfer Wrong
Frame-QL deliberately borrows SQL’s surface so that the syntax costs a SQL-literate reader nothing. The price of familiarity is that a handful of SQL intuitions transfer incorrectly, and they are worth naming once, plainly.
1. There is no GROUP BY, because the output anchor is the grouping. AT {customer, month} declares the grain of the result; grouping is implied by the ascription, and the anchor’s dimensions appear automatically as the frame’s leading columns. SELECT lists only the value series — listing a dimension in SELECT is the error, not the habit.
2. WHERE is not one table scan; it applies per series, at each series’ own input grain. In a multi-series query whose series draw from different roots, the predicate restricts each series’ input independently, its dimensions reached through verified hierarchy edges (Chapter 4.1). A predicate dimension unreachable from some series’ input grain is a clarification, not a silent partial filter.
3. There are no joins, and nothing is missing. Combining columns means bringing them to a common anchor — aggregate one side, broadcast the other — and mapping at that anchor. The relationship a SQL writer would join on was declared once, as a verified edge in a dimension family, and compiled into the Manifold; the fan trap is not avoided by care but unrepresentable by construction.
4. SUM may not equal the sum of the stored rows — on purpose, and it says so. Under the defaults (extensive reducer, MCAR missingness), the framework serves skip-and-rescale: an unbiased estimate of the population total, with the rescale factor disclosed in the annotation. Reconciling against a source system’s raw SUM? Pin the family’s missing-policy to skip (or read the factor out of the finding) — the difference is the declared missingness, made visible instead of silently undercounted.
5. Aliases are not variables — reuse is WITH, not SELECT-list chaining. An AS alias names an output column after reduction; it is invisible in WHERE and to sibling series. The reuse SQL writers reach for is the macro binding (WITH name = expression, Chapter 4.5): textual, expanded before everything, valid wherever the expanded expression would be.
Appendix D: Lineage — the retired terse @-fragment
Before the envelope shipped, Columna’s public query surface was a fragment of the language: a single output column, its output anchor spelled by a trailing @.
aov @ cal.month # the fragment: "aov, output-anchored at cal.month"
revenue, orders @ region # a short list, one shared trailing-@ output anchor
name: expr @ anchor # optional `:` label on a column
This form shipped in the pre-launch demo, the What-is-Columna glossary, the site query strings, and early manual drafts. It is retired, and it is not kept as a sugar. The reason is the one collision the envelope exists to kill: the fragment spelled two different things with @ — the outer trailing @ meant the output anchor (aov @ cal.month), while the inner @ of an inline reduction meant the input anchor (avg(aov@day)). Under the envelope’s ruling, @ {…} is the input-anchor marker universally and AT {…} is the sole output-grain declaration, so a top-level aov @ cal.month would now read as “aov input-anchored at cal.month, output anchor unstated” — an underdetermined frame. Keeping the fragment as sugar would mean @ spelled the output anchor in one position and the input anchor in every other; the envelope removes the ambiguity by removing the fragment.
Migration is mechanical. Every terse statement rewrites to the envelope by moving the trailing anchor into AT {…} and any : label into AS:
| Retired fragment | Envelope |
|---|---|
aov @ cal.month | SELECT aov AT {cal.month} |
revenue, orders @ region | SELECT revenue, orders AT {region} |
inv: level.last @ region | SELECT level.last AS inv AT {region} |
avg(aov@day) @ cal.month | SELECT avg(aov @ {day}) AT {cal.month} |
What the envelope absorbed from the fragment: the per-subexpression input anchor (@ {…}), the anchor product *, dotted member addressing (level.last), and juxtaposition of several readings at a shared anchor. What it declined: the trailing-@ output spelling and the : label — both replaced by the unambiguous AT {…} / AS. (This is the Coframe→envelope lineage note of ADR-035 D2: the ideas the shipped grammar kept, and the container ceremony it left behind.)
End of the Frame-QL Manual, First Edition (a renamed, lightly-revised continuation of the Coframe-QL Manual, Third Edition; reconciled to the shipped envelope grammar, ADR-035).