Skip to main content

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}