Common table expressions
A common table expression names a query so the rest of the statement can read it as if it
were a table. Select::cte(name) produces the handle, and the statement that uses it renders
the WITH clause itself.
The expression is defined exactly where it is used, so there is no second statement and no name to keep in sync. Its parameters come first, in the order the clause renders, and the statement's own follow — one statement, one round trip.
Columns
A table handle gets one field per column from #[derive(Table)], because a table's columns
are known when the program is compiled. An expression's name is chosen at the call site, so
there is nothing for the derive to expand. cte.column(..) supplies the missing half: it
takes the column handle of the table the expression selects from and returns the same column
qualified by the expression's name.
The Rust type, the SQL type and the nullability are the ones the schema declares, and the expression must be in the query's row set for the call to compile — using a handle the statement never defined or joined is a compile error.
Two expressions over the same table are the same Rust type, so the compiler cannot tell them apart. The check is "this kind of expression is in scope", not "this exact one"; a mistake there is a database error rather than a compile error. For the same reason a statement can reference two expressions over two different tables but not two over the same one — the row set would not say which is meant, exactly as it cannot for two joins of one table.
Joining one
inner_join_cte and left_join_cte are the join forms; a left join widens the row type to
Option<T> exactly as it does for a table. Joining attaches the definition to the statement,
so the WITH clause appears without a separate call.
Using one in a subquery
A statement can define an expression it only mentions inside a subquery with with_cte:
The method is with_cte rather than with, because Select::with already means eager
loading a relation.
Recursive expressions
WITH RECURSIVE needs the handle to be readable inside its own definition. Cte::union_all
takes the recursive term and builds the definition out of the anchor and that term:
The statement reads the name back out, so all() decodes Node rather than a union of the
two branches. .pick(nodes) on the recursive term is what keeps the branch's row type the
same as the anchor's: a join widens the projection to both sides, and a union requires every
branch to produce the same columns.
clone() is needed only because one handle is used twice in one statement; the handle is a
value, not a borrow.
What is checked
- A query that names an expression has to be a whole-row
SELECTover one table. That is what lets the handle know its columns, and therefore what makescte.column(..)typed. A projected query needs a different shape of handle, which does not exist yet. - Every branch of a recursive definition must select the same number of columns; the builder checks before sending.
- A reference to an expression that is not in the row set does not compile.
Gotchas
- The name is written into the SQL, not bound. It is a
&'static str, so it is a literal at the call site; never build one from user input. - A name is not prefixed.
Database::prefixedrewrites table names in built statements; an expression's name is not a table and is left alone, which is what makes it work under a prefix. - An expression's name is scoped to its statement. Two statements that both use
"adults"each define their own; there is nothing shared to get out of step. - The recursion limit is the database's. SQLite, PostgreSQL and MySQL each have their own
behaviour for a runaway recursive term; Besu adds no guard. MySQL stops at
cte_max_recursion_depth(1000 by default).
