squonk_ast/ast/pipe_ops.rs
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! BigQuery / ZetaSQL **query pipe syntax** — the `|>` operators that post-process a
5//! query result, one operator at a time (`FROM t |> WHERE x > 1 |> SELECT a, b`).
6//!
7//! # What this models
8//!
9//! A [`Query`] carries a trailing
10//! [`pipe_operators`](super::Query::pipe_operators) list — each element one `|>`
11//! operator applied left to right to the preceding query's result. The list is empty
12//! for an ordinary query (the common case, so the field is a bare `ThinVec` that costs
13//! one null pointer when unused, mirroring [`Query::locking`](super::Query)). This is
14//! deliberate parity with `sqlparser-rs`'s `Query.pipe_operators: Vec<PipeOperator>`.
15//!
16//! # NOT the `||` operator
17//!
18//! This [`PipeOperator`] enum is unrelated to
19//! [`dialect::PipeOperator`](crate::dialect::PipeOperator), which is the *meaning* tag
20//! for the `||` token (string-concat vs logical-OR). The names collide only across
21//! modules; nothing imports both by bare name. This file's `PipeOperator` is a query
22//! **AST node** keyed on the `|>` (pipe-arrow) separator, a distinct token
23//! (`Operator::PipeArrow`, defined in the tokenizer crate).
24//!
25//! # The `|>` separator (lexing)
26//!
27//! `|>` is a two-byte token munched in the tokenizer's `|` arm, ahead of a lone `|`
28//! and beside `||`, **gated on
29//! [`QueryTailSyntax::pipe_syntax`](crate::dialect::SelectSyntax)**. With the gate off
30//! (every shipped preset today) the two bytes stay `|` then `>` exactly as before, so
31//! no dialect's lexing shifts — the same feature-gated maximal-munch idiom the parser
32//! uses for `->`/`<=>`/`//`. The PostgreSQL geometric operator `|>>` ("is strictly
33//! above") is **not** lexed by this parser (it has no geometric-operator support). If
34//! geometric operators are added, they need their own dialect gate; no shipped preset
35//! enables both pipe syntax and PG geometry, so the two munches never contend under one
36//! `FeatureSet`.
37//!
38use super::{
39 Expr, Extension, FunctionCall, Ident, Join, NoExt, OrderByExpr, PivotColumn, PivotExpr, Query,
40 SelectItem, SetOperator, SetQuantifier, TableAlias, TableSample, ThinVec, UnpivotColumn,
41 UpdateAssignment,
42};
43use crate::vocab::Meta;
44
45/// One BigQuery/ZetaSQL `|>` pipe operator applied to a query result.
46///
47/// Each variant is one `|> <KEYWORD> …` step; a [`Query`] holds a list of
48/// them applied left to right. Generic over the extension parameter `X` because most
49/// operators carry expressions.
50#[derive(Clone, Debug, PartialEq, Eq, Hash)]
51#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
52#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
53pub enum PipeOperator<X: Extension = NoExt> {
54 /// `|> WHERE <predicate>` — keep only the rows for which `predicate` holds.
55 ///
56 /// The framework's reference operator: the minimal single-expression body, mirroring
57 /// an ordinary `WHERE` clause's one predicate. Its parse/render/test wiring is the
58 /// template every later operator arm follows.
59 Where {
60 /// Predicate that controls this clause.
61 predicate: Expr<X>,
62 /// Source location and node identity.
63 meta: Meta,
64 },
65 /// `|> SELECT <select-items>` — replace the current row shape with a new projection.
66 ///
67 /// Reuses [`SelectItem`], the ordinary `SELECT`-list item shape (an
68 /// expression with an optional alias, or a `*` / `t.*` wildcard), so a pipe projection
69 /// and a leading `SELECT` list carry identical nodes. ZetaSQL's `|> SELECT` also admits
70 /// the `DISTINCT` and `AS STRUCT | VALUE` modifiers; those modifiers are intentionally
71 /// absent from this item-only shape.
72 Select {
73 /// Child items in source order.
74 items: ThinVec<SelectItem<X>>,
75 /// Source location and node identity.
76 meta: Meta,
77 },
78 /// `|> EXTEND <select-items>` — append computed columns to the current row shape.
79 ///
80 /// The same operand shape as [`Select`](Self::Select) ([`SelectItem`]):
81 /// the difference is semantic — `EXTEND` keeps the existing columns and adds these, where
82 /// `SELECT` replaces them — not structural, so the two share the item node rather than a
83 /// parallel copy.
84 Extend {
85 /// Child items in source order.
86 items: ThinVec<SelectItem<X>>,
87 /// Source location and node identity.
88 meta: Meta,
89 },
90 /// `|> AS <alias>` — bind a range-variable name (table alias) to the current result.
91 ///
92 /// Reuses [`TableAlias`], the correlation-name shape a `FROM` item
93 /// carries. ZetaSQL's `|> AS` names only the range variable — it has no column-alias list
94 /// — so the reused node always carries an empty
95 /// [`columns`](super::TableAlias::columns): the parser never populates it, and a trailing
96 /// `(a, b)` is left unconsumed for the caller to reject.
97 As {
98 /// Alias assigned by this syntax.
99 alias: TableAlias,
100 /// Source location and node identity.
101 meta: Meta,
102 },
103 /// `|> ORDER BY <sort-keys>` — sort the current result.
104 ///
105 /// Reuses [`OrderByExpr`], the ordinary query `ORDER BY` sort-key
106 /// shape (`<expr> [ASC | DESC | USING <op>] [NULLS FIRST | LAST]`), so a pipe sort and a
107 /// query-tail sort carry identical keys.
108 OrderBy {
109 /// keys in source order.
110 keys: ThinVec<OrderByExpr<X>>,
111 /// Source location and node identity.
112 meta: Meta,
113 },
114 /// `|> LIMIT <count> [OFFSET <skip>]` — bound the current result to `count` rows,
115 /// optionally skipping the first `skip`.
116 ///
117 /// The narrow ZetaSQL pipe form: a required row-count expression and an optional `OFFSET`
118 /// skip expression, both restricted to the ordinary `LIMIT`-operand grammar (an integer
119 /// literal or a `?` placeholder). It deliberately does *not* reuse the full
120 /// [`Limit`](super::Limit) clause node — the pipe form has no `FETCH FIRST`, `PERCENT`,
121 /// `WITH TIES`, or MySQL `LIMIT a, b` spelling — so it models only the two operands the
122 /// surface carries rather than widening onto the clause shape.
123 ///
124 /// The two [`Expr`] operands are boxed (unlike the reference [`Where`](Self::Where)'s
125 /// single inline predicate): a two-`Expr` inline payload would be the lone fat variant of
126 /// an otherwise-small enum whose common variants hold a one-word `ThinVec`, so ADR-0007's
127 /// skew rule boxes it to keep [`PipeOperator`] lean — the allocation is paid only on the
128 /// rare pipe-`LIMIT` path.
129 Limit {
130 /// The `LIMIT` row count.
131 count: Box<Expr<X>>,
132 /// Row offset applied before returning results.
133 offset: Option<Box<Expr<X>>>,
134 /// Source location and node identity.
135 meta: Meta,
136 },
137 /// `|> [<join-type>] JOIN <relation> [ON <predicate> | USING (<cols>)]` — join the
138 /// current result to another relation.
139 ///
140 /// Reuses the ordinary [`Join`] node wholesale (its
141 /// [`relation`](super::Join::relation) table factor, its
142 /// [`operator`](super::Join::operator) side/kind spelling, and the embedded
143 /// `ON`/`USING` constraint), so a pipe join and a `FROM`-clause join carry identical
144 /// nodes — the pipe form admits exactly the join grammar the dialect's join parser
145 /// admits, nothing invented here. The whole [`Join`] is boxed (it is a 176-byte node,
146 /// far past [`PipeOperator`]'s 56-byte budget); the allocation is paid only on the
147 /// pipe-join path, exactly as [`Limit`](Self::Limit) boxes its operands (ADR-0007).
148 Join {
149 /// The reused join node — relation, operator, and constraint; see [`Join`].
150 join: Box<Join<X>>,
151 /// Source location and node identity.
152 meta: Meta,
153 },
154 /// `|> {UNION | INTERSECT | EXCEPT} [ALL | DISTINCT] (<query>) [, (<query>) …]` —
155 /// combine the current result with one or more parenthesized queries under a set
156 /// operation.
157 ///
158 /// One variant carries the [`SetOperator`] tag rather than three
159 /// operator-named variants — reusing the same `op: SetOperator` shape the ordinary
160 /// query set operation ([`SetExpr::SetOperation`](super::SetExpr)) uses, so the three
161 /// pipe set operations share this node exactly as the three query set operations share
162 /// theirs. The [`quantifier`](Self::SetOperation::quantifier) is
163 /// [`Option`]al — `None` renders the bare operator, `Some` the explicit `ALL`/`DISTINCT`
164 /// — capturing all three surfaces for an exact round-trip (unlike the query set op's
165 /// `all: bool`, which cannot distinguish a bare operator from an explicit `DISTINCT`).
166 /// The operand [`queries`](Self::SetOperation::queries) live in the `ThinVec`'s heap
167 /// buffer, so this variant costs only that one-word pointer inline.
168 SetOperation {
169 /// Operator applied by this expression.
170 op: SetOperator,
171 /// Optional quantifier for this syntax.
172 quantifier: Option<SetQuantifier>,
173 /// queries in source order.
174 queries: ThinVec<Query<X>>,
175 /// Source location and node identity.
176 meta: Meta,
177 },
178 /// `|> SET <column> = <expr> [, …]` — replace named columns of the current row shape
179 /// with new expressions.
180 ///
181 /// Reuses [`UpdateAssignment`], the `UPDATE … SET` assignment
182 /// node, but the parser builds only its [`Single`](super::UpdateAssignment::Single)
183 /// `<column> = <expr>` form: the ZetaSQL pipe surface has no multiple-column
184 /// `( … ) = <source>` tuple assignment and no `DEFAULT` right-hand side, so those
185 /// arms of the reused node are never populated here (one shape per construct, no
186 /// parallel copy).
187 Set {
188 /// assignments in source order.
189 assignments: ThinVec<UpdateAssignment<X>>,
190 /// Source location and node identity.
191 meta: Meta,
192 },
193 /// `|> CALL <function>(<args>) [AS <alias>]` — apply a table-valued function to the
194 /// current result, optionally binding a range-variable name to the output.
195 ///
196 /// Reuses [`FunctionCall`], the ordinary call node, for
197 /// `<function>(<args>)` (boxed — it is a 120-byte node, past the budget). The optional
198 /// trailing alias reuses [`TableAlias`] name-only, exactly as
199 /// [`As`](Self::As) does: the pipe `AS <alias>` names a range variable with no
200 /// column-alias list, so [`columns`](super::TableAlias::columns) is always empty and a
201 /// trailing `(a, b)` is left to reject. The alias is boxed because the variant already
202 /// carries the boxed call (an inline [`TableAlias`] would push it past the budget).
203 Call {
204 /// The table-function call applied to the pipe input; see [`FunctionCall`].
205 call: Box<FunctionCall<X>>,
206 /// Alias assigned by this syntax.
207 alias: Option<Box<TableAlias>>,
208 /// Source location and node identity.
209 meta: Meta,
210 },
211 /// `|> AGGREGATE <aggregates> [GROUP BY <keys>]` — collapse the current result into
212 /// aggregate rows, optionally grouped by the trailing keys.
213 ///
214 /// Both lists carry [`PipeAggregateExpr`] — an expression with an optional output
215 /// alias *and* an optional `ASC`/`DESC` + `NULLS FIRST`/`LAST` ordering suffix,
216 /// matching `sqlparser-rs`'s single `ExprWithAliasAndOrderBy` shape for the two
217 /// lists (its `full_table_exprs` / `group_by_expr`). ZetaSQL's pipe `AGGREGATE` folds
218 /// grouping and the aggregate-driven "GROUP AND ORDER BY" ordering into one operator,
219 /// so an aggregate or a grouping key may each name its own sort direction; a
220 /// dedicated combined item is used because neither [`SelectItem`]
221 /// (alias, no ordering) nor [`GroupByItem`](super::GroupByItem) (grouping semantics,
222 /// no alias/ordering) carries both. The [`aggregates`](Self::Aggregate::aggregates)
223 /// list is empty for a grouping-only `|> AGGREGATE GROUP BY x`; the
224 /// [`group_by`](Self::Aggregate::group_by) list is empty when no `GROUP BY` is
225 /// written. The `GROUP AND ORDER BY` keyword spelling and the bare (`AS`-less) item
226 /// alias are intentionally absent from this shape.
227 Aggregate {
228 /// aggregates in source order.
229 aggregates: ThinVec<PipeAggregateExpr<X>>,
230 /// Grouping terms in source order.
231 group_by: ThinVec<PipeAggregateExpr<X>>,
232 /// Source location and node identity.
233 meta: Meta,
234 },
235 /// `|> DROP <column> [, …]` — remove named columns from the current row shape.
236 ///
237 /// The columns are a bare [`Ident`] list, not
238 /// [`ObjectName`](super::ObjectName)s: ZetaSQL's `DROP` names output columns of the
239 /// current table by their unqualified name, so a qualified `t.c` has no meaning here.
240 Drop {
241 /// Columns in source order.
242 columns: ThinVec<Ident>,
243 /// Source location and node identity.
244 meta: Meta,
245 },
246 /// `|> RENAME <old> AS <new> [, …]` — rename columns of the current row shape.
247 ///
248 /// Each mapping is a [`PipeRenameItem`] pair of bare identifiers, kept distinct from a
249 /// projection alias ([`SelectItem::Expr`](super::SelectItem)'s `alias`): a rename
250 /// keeps the column's value and changes only its name, where a projection alias names
251 /// a computed item. A dedicated two-[`Ident`] shape is used rather than
252 /// the DuckDB wildcard-modifier [`WildcardRename`](super::WildcardRename), whose source
253 /// is a qualifiable [`ObjectName`](super::ObjectName) and which belongs to the `*
254 /// RENAME (…)` surface — reusing it here would both over-accept a qualified source and
255 /// conflate two unrelated features.
256 Rename {
257 /// renames in source order.
258 renames: ThinVec<PipeRenameItem>,
259 /// Source location and node identity.
260 meta: Meta,
261 },
262 /// `|> PIVOT (<aggregates> FOR <column> IN (<values>))` — rotate the distinct values
263 /// of one pivot column into columns, aggregating each cell.
264 ///
265 /// Reuses the shared pivot sub-shapes — [`PivotExpr`] for the
266 /// aggregate list and [`PivotColumn`] for the single `FOR <column>
267 /// IN (<values>)` head — rather than the whole [`Pivot`](super::Pivot) node: the pipe
268 /// operator has no `source` relation (it pivots the pipe input) and none of the
269 /// statement-only `WITH`/`ORDER BY`/`LIMIT` tail, so it carries only the parenthesized
270 /// body's two operands, keeping the table-factor `PIVOT` and the pipe `PIVOT`
271 /// unconflated. Exactly one `FOR` column (ZetaSQL admits no second head), and the
272 /// aggregate list is non-empty. The [`column`](Self::Pivot::column) is boxed — it
273 /// carries an [`Expr`] and would otherwise widen the variant past the
274 /// budget — while the aggregates ride the `ThinVec`'s heap buffer.
275 Pivot {
276 /// aggregates in source order.
277 aggregates: ThinVec<PivotExpr<X>>,
278 /// Column referenced by this syntax.
279 column: Box<PivotColumn<X>>,
280 /// Source location and node identity.
281 meta: Meta,
282 },
283 /// `|> UNPIVOT (<value> FOR <name> IN (<columns>))` — collapse a set of columns into
284 /// `name`/`value` row pairs (the inverse of [`Pivot`](Self::Pivot)).
285 ///
286 /// Reuses the shared [`UnpivotColumn`] sub-shape for the `IN`
287 /// list and mirrors the [`Unpivot`](super::Unpivot) core's `value`/`name` identifier
288 /// lists (each a `ThinVec<Ident>` so the multi-column `(v1, v2) FOR n IN ((a, b), (c,
289 /// d))` surface reuses one shape), rather than the whole `Unpivot` node: as with
290 /// [`Pivot`](Self::Pivot) the pipe operator has no `source` and no statement tail, so
291 /// it carries only the parenthesized body. The `INCLUDE`/`EXCLUDE NULLS` marker is
292 /// intentionally absent from this shape.
293 Unpivot {
294 /// The output *value* column name(s) (`<value>` before `FOR`); one for the common
295 /// form, several for a multi-column unpivot.
296 value: ThinVec<Ident>,
297 /// The output *name* column name(s) (`<name>` after `FOR`); one in the common
298 /// form.
299 name: ThinVec<Ident>,
300 /// Columns in source order.
301 columns: ThinVec<UnpivotColumn<X>>,
302 /// Source location and node identity.
303 meta: Meta,
304 },
305 /// `|> TABLESAMPLE <method> (<args>) [REPEATABLE (<seed>)]` — sample the current
306 /// result.
307 ///
308 /// Reuses the whole [`TableSample`] node (the `FROM`-clause
309 /// sampling suffix), boxed — it is a wide node past the budget, and the allocation is
310 /// paid only on the pipe-sample path, exactly as [`Join`](Self::Join) boxes its reused
311 /// node. The pipe form admits precisely the argument grammar the table-factor
312 /// `TABLESAMPLE` admits (a bare numeric percentage rather than the BigQuery
313 /// `PERCENT`/`ROWS` unit keyword, which the reused shape has no field for).
314 TableSample {
315 /// The `TABLESAMPLE` clause; see [`TableSample`].
316 sample: Box<TableSample<X>>,
317 /// Source location and node identity.
318 meta: Meta,
319 },
320}
321
322/// One item of a pipe [`AGGREGATE`](PipeOperator::Aggregate) operator's aggregate list or
323/// `GROUP BY` list: an expression with an optional output alias and an optional
324/// `ASC`/`DESC` + `NULLS FIRST`/`LAST` ordering suffix.
325///
326/// The single shape both lists share, matching `sqlparser-rs`'s
327/// `ExprWithAliasAndOrderBy` (an aliased expression plus `OrderByOptions`): ZetaSQL's
328/// pipe `AGGREGATE` lets an aggregate drive output ordering and lets a grouping key carry
329/// its own direction, so the alias and the ordering co-occur on one item. Modelled
330/// separately from [`OrderByExpr`] (which has no alias and carries the
331/// PostgreSQL `USING <op>` sort form the pipe operator has no grammar for) and from
332/// [`SelectItem`] (which carries no ordering).
333#[derive(Clone, Debug, PartialEq, Eq, Hash)]
334#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
335#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
336pub struct PipeAggregateExpr<X: Extension = NoExt> {
337 /// Expression evaluated by this syntax.
338 pub expr: Expr<X>,
339 /// Alias assigned by this syntax.
340 pub alias: Option<Ident>,
341 /// `ASC` (`Some(true)`) / `DESC` (`Some(false)`), or `None` when unwritten — the
342 /// ordering direction, mirroring [`OrderByExpr::asc`](super::OrderByExpr).
343 pub asc: Option<bool>,
344 /// `NULLS FIRST` (`Some(true)`) / `NULLS LAST` (`Some(false)`), or `None` when
345 /// unwritten, mirroring [`OrderByExpr::nulls_first`](super::OrderByExpr).
346 pub nulls_first: Option<bool>,
347 /// Source location and node identity.
348 pub meta: Meta,
349}
350
351/// One `<old> AS <new>` mapping of a pipe [`RENAME`](PipeOperator::Rename) operator: the
352/// source column [`old`](Self::old) renamed to the output name [`new`](Self::new).
353///
354/// Both sides are bare [`Ident`]s — the pipe `RENAME` renames output columns
355/// of the current table by their unqualified name — which is what keeps a rename distinct
356/// from both a projection alias and the qualifiable DuckDB wildcard
357/// [`WildcardRename`](super::WildcardRename).
358#[derive(Clone, Debug, PartialEq, Eq, Hash)]
359#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
360#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
361pub struct PipeRenameItem {
362 /// The source column being renamed.
363 pub old: Ident,
364 /// The new output column name.
365 pub new: Ident,
366 /// Source location and node identity.
367 pub meta: Meta,
368}