STET

agent_patch

runs/2026-02-28__18-27-22__gpt-5-1-codex-mini/flux-pr-1759/flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/agent-logs/agent.patch

38120 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/mod.rs b/app/src/ast/mod.rs
index d658898..83c4e51 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/mod.rs
+++ b/app/src/ast/mod.rs
@@ -66,13 +66,13 @@ pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert};
 pub use self::operator::{BinaryOperator, UnaryOperator};
 pub use self::query::{
     AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
-    ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, Fetch, ForClause, ForJson, ForXml,
+    ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, ForJson, ForXml,
     FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem,
     InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator,
     JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn,
     LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol,
     Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows,
-    OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource,
+    OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource, PipeOperator,
     ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement,
     ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem,
     SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting,
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/query.rs b/app/src/ast/query.rs
index 982985e..c192df9 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -62,6 +62,8 @@ pub struct Query {
     /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/format)
     /// (ClickHouse-specific)
     pub format_clause: Option<FormatClause>,
+    /// Pipe operators applied to the query.
+    pub pipe_operators: Vec<PipeOperator>,
 }
 
 impl fmt::Display for Query {
@@ -92,6 +94,10 @@ impl fmt::Display for Query {
         if let Some(ref format) = self.format_clause {
             write!(f, " {}", format)?;
         }
+        for pipe_operator in &self.pipe_operators {
+            write!(f, " |> ")?;
+            pipe_operator.fmt(f)?;
+        }
         Ok(())
     }
 }
@@ -1004,6 +1010,28 @@ impl fmt::Display for ExprWithAlias {
     }
 }
 
+/// An expression optionally followed by an alias and order by options.
+///
+/// Example:
+/// ```sql
+/// 42 AS myint ASC
+/// ```
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct ExprWithAliasAndOrderBy {
+    /// Expression with optional alias.
+    pub expr: ExprWithAlias,
+    /// Ordering options applied to the expression.
+    pub order_by: OrderByOptions,
+}
+
+impl fmt::Display for ExprWithAliasAndOrderBy {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}{}", self.expr, self.order_by)
+    }
+}
+
 /// Arguments to a table-valued function
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -2478,6 +2506,344 @@ impl fmt::Display for LimitClause {
     }
 }
 
+/// Pipe syntax, first introduced in Google BigQuery.
+/// Example:
+///
+/// ```sql
+/// FROM Produce
+/// |> WHERE sales > 0
+/// |> AGGREGATE SUM(sales) AS total_sales, COUNT(*) AS num_sales
+///    GROUP BY item;
+/// ```
+///
+/// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#pipe_syntax>
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum PipeOperator {
+    /// Limits the number of rows to return in a query, with an optional OFFSET clause to skip over rows.
+    ///
+    /// Syntax: `|> LIMIT <n> [OFFSET <m>]`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#limit_pipe_operator>
+    Limit {
+        /// The expression specifying the number of rows to return.
+        expr: Expr,
+        /// Optional offset expression provided inline with `LIMIT`.
+        offset: Option<Expr>,
+    },
+    /// Filters the results of the input table.
+    ///
+    /// Syntax: `|> WHERE <condition>`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#where_pipe_operator>
+    Where {
+        /// The filter expression.
+        expr: Expr,
+    },
+    /// `ORDER BY <expr> [ASC|DESC], ...`
+    OrderBy {
+        /// The ordering expressions.
+        exprs: Vec<OrderByExpr>,
+    },
+    /// Produces a new table with the listed columns, similar to the outermost SELECT clause in a table subquery in standard syntax.
+    ///
+    /// Syntax `|> SELECT <expr> [[AS] alias], ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#select_pipe_operator>
+    Select {
+        /// The select items to produce.
+        exprs: Vec<SelectItem>,
+    },
+    /// Propagates the existing table and adds computed columns, similar to SELECT *, new_column in standard syntax.
+    ///
+    /// Syntax: `|> EXTEND <expr> [[AS] alias], ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#extend_pipe_operator>
+    Extend {
+        /// Expressions defining added columns.
+        exprs: Vec<SelectItem>,
+    },
+    /// Replaces the value of a column in the current table, similar to SELECT * REPLACE (expression AS column) in standard syntax.
+    ///
+    /// Syntax: `|> SET <column> = <expression>, ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#set_pipe_operator>
+    Set {
+        /// Assignments to apply (`column = expr`).
+        assignments: Vec<Assignment>,
+    },
+    /// Removes listed columns from the current table, similar to SELECT * EXCEPT (column) in standard syntax.
+    ///
+    /// Syntax: `|> DROP <column>, ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#drop_pipe_operator>
+    Drop {
+        /// Columns to drop.
+        columns: Vec<Ident>,
+    },
+    /// Introduces a table alias for the input table, similar to applying the AS alias clause on a table subquery in standard syntax.
+    ///
+    /// Syntax: `|> AS <alias>`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#as_pipe_operator>
+    As {
+        /// Alias to assign to the input table.
+        alias: Ident,
+    },
+    /// Performs aggregation on data across grouped rows or an entire table.
+    ///
+    /// Syntax: `|> AGGREGATE <agg_expr> [[AS] alias], ...`
+    ///
+    /// Syntax:
+    /// ```norust
+    /// |> AGGREGATE [<agg_expr> [[AS] alias], ...]
+    /// GROUP BY <grouping_expr> [AS alias], ...
+    /// ```
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#aggregate_pipe_operator>
+    Aggregate {
+        /// Expressions computed for each row prior to grouping.
+        full_table_exprs: Vec<ExprWithAliasAndOrderBy>,
+        /// Grouping expressions for aggregation.
+        group_by_expr: Vec<ExprWithAliasAndOrderBy>,
+    },
+    /// Selects a random sample of rows from the input table.
+    /// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT)
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#tablesample_pipe_operator>
+    TableSample {
+        /// Sampling clause describing the sample.
+        sample: Box<TableSample>,
+    },
+    /// Renames columns in the input table.
+    ///
+    /// Syntax: `|> RENAME old_name AS new_name, ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#rename_pipe_operator>
+    Rename {
+        /// Mappings of old to new identifiers.
+        mappings: Vec<IdentWithAlias>,
+    },
+    /// Combines the input table with one or more tables using UNION.
+    ///
+    /// Syntax: `|> UNION [ALL|DISTINCT] (<query>), (<query>), ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#union_pipe_operator>
+    Union {
+        /// Set quantifier (`ALL` or `DISTINCT`).
+        set_quantifier: SetQuantifier,
+        /// The queries to combine with `UNION`.
+        queries: Vec<Query>,
+    },
+    /// Returns only the rows that are present in both the input table and the specified tables.
+    ///
+    /// Syntax: `|> INTERSECT [DISTINCT] (<query>), (<query>), ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#intersect_pipe_operator>
+    Intersect {
+        /// Set quantifier for the `INTERSECT` operator.
+        set_quantifier: SetQuantifier,
+        /// The queries to intersect.
+        queries: Vec<Query>,
+    },
+    /// Returns only the rows that are present in the input table but not in the specified tables.
+    ///
+    /// Syntax: `|> EXCEPT DISTINCT (<query>), (<query>), ...`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#except_pipe_operator>
+    Except {
+        /// Set quantifier for the `EXCEPT` operator.
+        set_quantifier: SetQuantifier,
+        /// The queries to exclude from the input set.
+        queries: Vec<Query>,
+    },
+    /// Calls a table function or procedure that returns a table.
+    ///
+    /// Syntax: `|> CALL function_name(args) [AS alias]`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#call_pipe_operator>
+    Call {
+        /// The function or procedure to call which returns a table.
+        function: Function,
+        /// Optional alias for the result table.
+        alias: Option<Ident>,
+    },
+    /// Pivots data from rows to columns.
+    ///
+    /// Syntax: `|> PIVOT(aggregate_function(column) FOR pivot_column IN (value1, value2, ...)) [AS alias]`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#pivot_pipe_operator>
+    Pivot {
+        /// Aggregate functions to compute during pivot.
+        aggregate_functions: Vec<ExprWithAlias>,
+        /// Column(s) that provide the pivot values.
+        value_column: Vec<Ident>,
+        /// The source of pivot values (literal list or subquery).
+        value_source: PivotValueSource,
+        /// Optional alias for the output.
+        alias: Option<Ident>,
+    },
+    /// The `UNPIVOT` pipe operator transforms columns into rows.
+    ///
+    /// Syntax:
+    /// ```sql
+    /// |> UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias]
+    /// ```
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#unpivot_pipe_operator>
+    Unpivot {
+        /// Output column that will receive the unpivoted value.
+        value_column: Ident,
+        /// Column name holding the unpivoted column name.
+        name_column: Ident,
+        /// Columns to unpivot.
+        unpivot_columns: Vec<Ident>,
+        /// Optional alias for the unpivot result.
+        alias: Option<Ident>,
+    },
+    /// Joins the input table with another table.
+    ///
+    /// Syntax: `|> [JOIN_TYPE] JOIN <table> [alias] ON <condition>` or `|> [JOIN_TYPE] JOIN <table> [alias] USING (<columns>)`
+    ///
+    /// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#join_pipe_operator>
+    Join(Join),
+}
+
+impl fmt::Display for PipeOperator {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            PipeOperator::Select { exprs } => {
+                write!(f, "SELECT {}", display_comma_separated(exprs.as_slice()))
+            }
+            PipeOperator::Extend { exprs } => {
+                write!(f, "EXTEND {}", display_comma_separated(exprs.as_slice()))
+            }
+            PipeOperator::Set { assignments } => {
+                write!(f, "SET {}", display_comma_separated(assignments.as_slice()))
+            }
+            PipeOperator::Drop { columns } => {
+                write!(f, "DROP {}", display_comma_separated(columns.as_slice()))
+            }
+            PipeOperator::As { alias } => {
+                write!(f, "AS {alias}")
+            }
+            PipeOperator::Limit { expr, offset } => {
+                write!(f, "LIMIT {expr}")?;
+                if let Some(offset) = offset {
+                    write!(f, " OFFSET {offset}")?;
+                }
+                Ok(())
+            }
+            PipeOperator::Aggregate {
+                full_table_exprs,
+                group_by_expr,
+            } => {
+                write!(f, "AGGREGATE")?;
+                if !full_table_exprs.is_empty() {
+                    write!(
+                        f,
+                        " {}",
+                        display_comma_separated(full_table_exprs.as_slice())
+                    )?;
+                }
+                if !group_by_expr.is_empty() {
+                    write!(f, " GROUP BY {}", display_comma_separated(group_by_expr))?;
+                }
+                Ok(())
+            }
+
+            PipeOperator::Where { expr } => {
+                write!(f, "WHERE {expr}")
+            }
+            PipeOperator::OrderBy { exprs } => {
+                write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice()))
+            }
+
+            PipeOperator::TableSample { sample } => {
+                write!(f, "{sample}")
+            }
+            PipeOperator::Rename { mappings } => {
+                write!(f, "RENAME {}", display_comma_separated(mappings))
+            }
+            PipeOperator::Union {
+                set_quantifier,
+                queries,
+            } => Self::fmt_set_operation(f, "UNION", set_quantifier, queries),
+            PipeOperator::Intersect {
+                set_quantifier,
+                queries,
+            } => Self::fmt_set_operation(f, "INTERSECT", set_quantifier, queries),
+            PipeOperator::Except {
+                set_quantifier,
+                queries,
+            } => Self::fmt_set_operation(f, "EXCEPT", set_quantifier, queries),
+            PipeOperator::Call { function, alias } => {
+                write!(f, "CALL {function}")?;
+                Self::fmt_optional_alias(f, alias)
+            }
+            PipeOperator::Pivot {
+                aggregate_functions,
+                value_column,
+                value_source,
+                alias,
+            } => {
+                write!(
+                    f,
+                    "PIVOT({} FOR {} IN ({}))",
+                    display_comma_separated(aggregate_functions),
+                    Expr::CompoundIdentifier(value_column.to_vec()),
+                    value_source
+                )?;
+                Self::fmt_optional_alias(f, alias)
+            }
+            PipeOperator::Unpivot {
+                value_column,
+                name_column,
+                unpivot_columns,
+                alias,
+            } => {
+                write!(
+                    f,
+                    "UNPIVOT({} FOR {} IN ({}))",
+                    value_column,
+                    name_column,
+                    display_comma_separated(unpivot_columns)
+                )?;
+                Self::fmt_optional_alias(f, alias)
+            }
+            PipeOperator::Join(join) => write!(f, "{join}"),
+        }
+    }
+}
+
+impl PipeOperator {
+    /// Helper function to format optional alias for pipe operators
+    fn fmt_optional_alias(f: &mut fmt::Formatter<'_>, alias: &Option<Ident>) -> fmt::Result {
+        if let Some(alias) = alias {
+            write!(f, " AS {alias}")?;
+        }
+        Ok(())
+    }
+
+    /// Helper function to format set operations (UNION, INTERSECT, EXCEPT) with queries
+    fn fmt_set_operation(
+        f: &mut fmt::Formatter<'_>,
+        operation: &str,
+        set_quantifier: &SetQuantifier,
+        queries: &[Query],
+    ) -> fmt::Result {
+        write!(f, "{operation}")?;
+        match set_quantifier {
+            SetQuantifier::None => {}
+            _ => {
+                write!(f, " {set_quantifier}")?;
+            }
+        }
+        write!(f, " {}", display_comma_separated(queries))?;
+        Ok(())
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/spans.rs b/app/src/ast/spans.rs
index 28d479f..8ca3ea7 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/spans.rs
+++ b/app/src/ast/spans.rs
@@ -102,6 +102,7 @@ impl Spanned for Query {
             for_clause: _,    // todo, mssql specific
             settings: _,      // todo, clickhouse specific
             format_clause: _, // todo, clickhouse specific
+            pipe_operators: _, // todo bigquery specific
         } = self;
 
         union_spans(
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/dialect/bigquery.rs b/app/src/dialect/bigquery.rs
index 49fb24f..29d7651 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/dialect/bigquery.rs
+++ b/app/src/dialect/bigquery.rs
@@ -118,6 +118,11 @@ impl Dialect for BigQueryDialect {
         true
     }
 
+    /// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax-guide#basic_syntax>
+    fn supports_pipe_operator(&self) -> bool {
+        true
+    }
+
     /// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#execute_immediate>
     fn supports_execute_immediate(&self) -> bool {
         true
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/dialect/mod.rs b/app/src/dialect/mod.rs
index b2dff06..b754a04 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/dialect/mod.rs
+++ b/app/src/dialect/mod.rs
@@ -518,6 +518,20 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Return true if the dialect supports pipe operator.
+    ///
+    /// Example:
+    /// ```sql
+    /// SELECT *
+    /// FROM table
+    /// |> limit 1
+    /// ```
+    ///
+    /// See <https://cloud.google.com/bigquery/docs/pipe-syntax-guide#basic_syntax>
+    fn supports_pipe_operator(&self) -> bool {
+        false
+    }
+
     /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax?
     fn supports_user_host_grantee(&self) -> bool {
         false
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/parser/mod.rs b/app/src/parser/mod.rs
index 03ea91f..8b5bea1 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -9554,6 +9554,14 @@ impl<'a> Parser<'a> {
         Ok(IdentWithAlias { ident, alias })
     }
 
+    /// Parse `identifier [AS] identifier` where the AS keyword is optional
+    fn parse_identifier_with_optional_alias(&mut self) -> Result<IdentWithAlias, ParserError> {
+        let ident = self.parse_identifier()?;
+        let _after_as = self.parse_keyword(Keyword::AS);
+        let alias = self.parse_identifier()?;
+        Ok(IdentWithAlias { ident, alias })
+    }
+
     /// Optionally parses an alias for a select list item
     fn maybe_parse_select_item_alias(&mut self) -> Result<Option<Ident>, ParserError> {
         fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
@@ -10571,6 +10579,7 @@ impl<'a> Parser<'a> {
                 for_clause: None,
                 settings: None,
                 format_clause: None,
+                pipe_operators: vec![],
             }
             .into())
         } else if self.parse_keyword(Keyword::UPDATE) {
@@ -10584,6 +10593,7 @@ impl<'a> Parser<'a> {
                 for_clause: None,
                 settings: None,
                 format_clause: None,
+                pipe_operators: vec![],
             }
             .into())
         } else if self.parse_keyword(Keyword::DELETE) {
@@ -10597,6 +10607,7 @@ impl<'a> Parser<'a> {
                 for_clause: None,
                 settings: None,
                 format_clause: None,
+                pipe_operators: vec![],
             }
             .into())
         } else {
@@ -10637,6 +10648,12 @@ impl<'a> Parser<'a> {
                 None
             };
 
+            let pipe_operators = if self.dialect.supports_pipe_operator() {
+                self.parse_pipe_operators()?
+            } else {
+                Vec::new()
+            };
+
             Ok(Query {
                 with,
                 body,
@@ -10647,6 +10664,7 @@ impl<'a> Parser<'a> {
                 for_clause,
                 settings,
                 format_clause,
+                pipe_operators,
             }
             .into())
         }
@@ -10669,6 +10687,259 @@ impl<'a> Parser<'a> {
         Ok(settings)
     }
 
+    /// Parse comma-separated list of parenthesized queries for pipe operators
+    fn parse_pipe_operator_queries(&mut self) -> Result<Vec<Query>, ParserError> {
+        self.parse_comma_separated(|parser| {
+            parser.expect_token(&Token::LParen)?;
+            let query = parser.parse_query()?;
+            parser.expect_token(&Token::RParen)?;
+            Ok(*query)
+        })
+    }
+
+    /// Parse set quantifier for pipe operators that require DISTINCT. E.g. INTERSECT and EXCEPT
+    fn parse_distinct_required_set_quantifier(
+        &mut self,
+        operator_name: &str,
+    ) -> Result<SetQuantifier, ParserError> {
+        let quantifier = self.parse_set_quantifier(&Some(SetOperator::Intersect));
+        match quantifier {
+            SetQuantifier::Distinct | SetQuantifier::DistinctByName => Ok(quantifier),
+            _ => Err(ParserError::ParserError(format!(
+                "{operator_name} pipe operator requires DISTINCT modifier",
+            ))),
+        }
+    }
+
+    /// Parse optional identifier alias (with or without AS keyword)
+    fn parse_identifier_optional_alias(&mut self) -> Result<Option<Ident>, ParserError> {
+        if self.parse_keyword(Keyword::AS) {
+            Ok(Some(self.parse_identifier()?))
+        } else {
+            // Check if the next token is an identifier (implicit alias)
+            self.maybe_parse(|parser| parser.parse_identifier())
+        }
+    }
+
+    fn parse_pipe_operators(&mut self) -> Result<Vec<PipeOperator>, ParserError> {
+        let mut pipe_operators = Vec::new();
+
+        while self.consume_token(&Token::VerticalBarRightAngleBracket) {
+            let kw = self.expect_one_of_keywords(&[
+                Keyword::SELECT,
+                Keyword::EXTEND,
+                Keyword::SET,
+                Keyword::DROP,
+                Keyword::AS,
+                Keyword::WHERE,
+                Keyword::LIMIT,
+                Keyword::AGGREGATE,
+                Keyword::ORDER,
+                Keyword::TABLESAMPLE,
+                Keyword::RENAME,
+                Keyword::UNION,
+                Keyword::INTERSECT,
+                Keyword::EXCEPT,
+                Keyword::CALL,
+                Keyword::PIVOT,
+                Keyword::UNPIVOT,
+                Keyword::JOIN,
+                Keyword::INNER,
+                Keyword::LEFT,
+                Keyword::RIGHT,
+                Keyword::FULL,
+                Keyword::CROSS,
+            ])?;
+            match kw {
+                Keyword::SELECT => {
+                    let exprs = self.parse_comma_separated(Parser::parse_select_item)?;
+                    pipe_operators.push(PipeOperator::Select { exprs })
+                }
+                Keyword::EXTEND => {
+                    let exprs = self.parse_comma_separated(Parser::parse_select_item)?;
+                    pipe_operators.push(PipeOperator::Extend { exprs })
+                }
+                Keyword::SET => {
+                    let assignments = self.parse_comma_separated(Parser::parse_assignment)?;
+                    pipe_operators.push(PipeOperator::Set { assignments })
+                }
+                Keyword::DROP => {
+                    let columns = self.parse_identifiers()?;
+                    pipe_operators.push(PipeOperator::Drop { columns })
+                }
+                Keyword::AS => {
+                    let alias = self.parse_identifier()?;
+                    pipe_operators.push(PipeOperator::As { alias })
+                }
+                Keyword::WHERE => {
+                    let expr = self.parse_expr()?;
+                    pipe_operators.push(PipeOperator::Where { expr })
+                }
+                Keyword::LIMIT => {
+                    let expr = self.parse_expr()?;
+                    let offset = if self.parse_keyword(Keyword::OFFSET) {
+                        Some(self.parse_expr()?)
+                    } else {
+                        None
+                    };
+                    pipe_operators.push(PipeOperator::Limit { expr, offset })
+                }
+                Keyword::AGGREGATE => {
+                    let full_table_exprs = if self.peek_keyword(Keyword::GROUP) {
+                        vec![]
+                    } else {
+                        self.parse_comma_separated(|parser| {
+                            parser.parse_expr_with_alias_and_order_by()
+                        })?
+                    };
+
+                    let group_by_expr = if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) {
+                        self.parse_comma_separated(|parser| {
+                            parser.parse_expr_with_alias_and_order_by()
+                        })?
+                    } else {
+                        vec![]
+                    };
+
+                    pipe_operators.push(PipeOperator::Aggregate {
+                        full_table_exprs,
+                        group_by_expr,
+                    })
+                }
+                Keyword::ORDER => {
+                    self.expect_one_of_keywords(&[Keyword::BY])?;
+                    let exprs = self.parse_comma_separated(Parser::parse_order_by_expr)?;
+                    pipe_operators.push(PipeOperator::OrderBy { exprs })
+                }
+                Keyword::TABLESAMPLE => {
+                    let sample = self.parse_table_sample(TableSampleModifier::TableSample)?;
+                    pipe_operators.push(PipeOperator::TableSample { sample });
+                }
+                Keyword::RENAME => {
+                    let mappings =
+                        self.parse_comma_separated(Parser::parse_identifier_with_optional_alias)?;
+                    pipe_operators.push(PipeOperator::Rename { mappings });
+                }
+                Keyword::UNION => {
+                    let set_quantifier = self.parse_set_quantifier(&Some(SetOperator::Union));
+                    let queries = self.parse_pipe_operator_queries()?;
+                    pipe_operators.push(PipeOperator::Union {
+                        set_quantifier,
+                        queries,
+                    });
+                }
+                Keyword::INTERSECT => {
+                    let set_quantifier =
+                        self.parse_distinct_required_set_quantifier("INTERSECT")?;
+                    let queries = self.parse_pipe_operator_queries()?;
+                    pipe_operators.push(PipeOperator::Intersect {
+                        set_quantifier,
+                        queries,
+                    });
+                }
+                Keyword::EXCEPT => {
+                    let set_quantifier = self.parse_distinct_required_set_quantifier("EXCEPT")?;
+                    let queries = self.parse_pipe_operator_queries()?;
+                    pipe_operators.push(PipeOperator::Except {
+                        set_quantifier,
+                        queries,
+                    });
+                }
+                Keyword::CALL => {
+                    let function_name = self.parse_object_name(false)?;
+                    let function_expr = self.parse_function(function_name)?;
+                    if let Expr::Function(function) = function_expr {
+                        let alias = self.parse_identifier_optional_alias()?;
+                        pipe_operators.push(PipeOperator::Call { function, alias });
+                    } else {
+                        return Err(ParserError::ParserError(
+                            "Expected function call after CALL".to_string(),
+                        ));
+                    }
+                }
+                Keyword::PIVOT => {
+                    self.expect_token(&Token::LParen)?;
+                    let aggregate_functions =
+                        self.parse_comma_separated(Self::parse_pivot_aggregate_function)?;
+                    self.expect_keyword_is(Keyword::FOR)?;
+                    let value_column = self.parse_period_separated(|p| p.parse_identifier())?;
+                    self.expect_keyword_is(Keyword::IN)?;
+
+                    self.expect_token(&Token::LParen)?;
+                    let value_source = if self.parse_keyword(Keyword::ANY) {
+                        let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
+                            self.parse_comma_separated(Parser::parse_order_by_expr)?
+                        } else {
+                            vec![]
+                        };
+                        PivotValueSource::Any(order_by)
+                    } else if self.peek_sub_query() {
+                        PivotValueSource::Subquery(self.parse_query()?)
+                    } else {
+                        PivotValueSource::List(
+                            self.parse_comma_separated(Self::parse_expr_with_alias)?,
+                        )
+                    };
+                    self.expect_token(&Token::RParen)?;
+                    self.expect_token(&Token::RParen)?;
+
+                    let alias = self.parse_identifier_optional_alias()?;
+
+                    pipe_operators.push(PipeOperator::Pivot {
+                        aggregate_functions,
+                        value_column,
+                        value_source,
+                        alias,
+                    });
+                }
+                Keyword::UNPIVOT => {
+                    self.expect_token(&Token::LParen)?;
+                    let value_column = self.parse_identifier()?;
+                    self.expect_keyword(Keyword::FOR)?;
+                    let name_column = self.parse_identifier()?;
+                    self.expect_keyword(Keyword::IN)?;
+
+                    self.expect_token(&Token::LParen)?;
+                    let unpivot_columns = self.parse_comma_separated(Parser::parse_identifier)?;
+                    self.expect_token(&Token::RParen)?;
+
+                    self.expect_token(&Token::RParen)?;
+
+                    let alias = self.parse_identifier_optional_alias()?;
+
+                    pipe_operators.push(PipeOperator::Unpivot {
+                        value_column,
+                        name_column,
+                        unpivot_columns,
+                        alias,
+                    });
+                }
+                Keyword::JOIN
+                | Keyword::INNER
+                | Keyword::LEFT
+                | Keyword::RIGHT
+                | Keyword::FULL
+                | Keyword::CROSS => {
+                    self.prev_token();
+                    let mut joins = self.parse_joins()?;
+                    if joins.len() != 1 {
+                        return Err(ParserError::ParserError(
+                            "Join pipe operator must have a single join".to_string(),
+                        ));
+                    }
+                    let join = joins.swap_remove(0);
+                    pipe_operators.push(PipeOperator::Join(join))
+                }
+                unhandled => {
+                    return Err(ParserError::ParserError(format!(
+                    "`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}"
+                )))
+                }
+            }
+        }
+        Ok(pipe_operators)
+    }
+
     /// Parse a mssql `FOR [XML | JSON | BROWSE]` clause
     pub fn parse_for_clause(&mut self) -> Result<Option<ForClause>, ParserError> {
         if self.parse_keyword(Keyword::XML) {
@@ -12122,6 +12393,7 @@ impl<'a> Parser<'a> {
                     for_clause: None,
                     settings: None,
                     format_clause: None,
+                    pipe_operators: vec![],
                 }),
                 alias,
             })
@@ -12286,6 +12558,13 @@ impl<'a> Parser<'a> {
             return Ok(None);
         };
 
+        self.parse_table_sample(modifier).map(Some)
+    }
+
+    fn parse_table_sample(
+        &mut self,
+        modifier: TableSampleModifier,
+    ) -> Result<Box<TableSample>, ParserError> {
         let name = match self.parse_one_of_keywords(&[
             Keyword::BERNOULLI,
             Keyword::ROW,
@@ -12367,14 +12646,14 @@ impl<'a> Parser<'a> {
             None
         };
 
-        Ok(Some(Box::new(TableSample {
+        Ok(Box::new(TableSample {
             modifier,
             name,
             quantity,
             seed,
             bucket,
             offset,
-        })))
+        }))
     }
 
     fn parse_table_sample_seed(
@@ -12902,6 +13181,26 @@ impl<'a> Parser<'a> {
         Ok(ExprWithAlias { expr, alias })
     }
 
+    /// Parse expression with optional alias and order by.
+    pub fn parse_expr_with_alias_and_order_by(
+        &mut self,
+    ) -> Result<ExprWithAliasAndOrderBy, ParserError> {
+        let expr = self.parse_expr()?;
+
+        fn validator(explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
+            explicit || !&[Keyword::ASC, Keyword::DESC, Keyword::GROUP].contains(kw)
+        }
+        let alias = self.parse_optional_alias_inner(None, validator)?;
+        let order_by = OrderByOptions {
+            asc: self.parse_asc_desc(),
+            nulls_first: None,
+        };
+        Ok(ExprWithAliasAndOrderBy {
+            expr: ExprWithAlias { expr, alias },
+            order_by,
+        })
+    }
+
     pub fn parse_pivot_table_factor(
         &mut self,
         table: TableFactor,
diff --git a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/tokenizer.rs b/app/src/tokenizer.rs
index 13bce0c..1fdb9ad 100644
--- a/tmp/agent-patch-flux-pr-1759.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/tokenizer.rs
+++ b/app/src/tokenizer.rs
@@ -246,6 +246,8 @@ pub enum Token {
     ShiftLeftVerticalBar,
     /// `|>> PostgreSQL/Redshift geometrical binary operator (Is strictly above?)
     VerticalBarShiftRight,
+    /// `|>` BigQuery (pipe) operator
+    VerticalBarRightAngleBracket,
     /// `#>>`, extracts JSON sub-object at the specified path as text
     HashLongArrow,
     /// jsonb @> jsonb -> boolean: Test whether left json contains the right json
@@ -368,6 +370,7 @@ impl fmt::Display for Token {
             Token::TildeEqual => f.write_str("~="),
             Token::ShiftLeftVerticalBar => f.write_str("<<|"),
             Token::VerticalBarShiftRight => f.write_str("|>>"),
+            Token::VerticalBarRightAngleBracket => f.write_str("|>"),
             Token::Placeholder(ref s) => write!(f, "{s}"),
             Token::Arrow => write!(f, "->"),
             Token::LongArrow => write!(f, "->>"),
@@ -1392,16 +1395,18 @@ impl<'a> Tokenizer<'a> {
                                 _ => self.start_binop_opt(chars, "|&", None),
                             }
                         }
-                        Some('>') if self.dialect.supports_geometric_types() => {
+                        Some('>') => {
                             chars.next(); // consume
-                            match chars.peek() {
-                                Some('>') => self.consume_for_binop(
-                                    chars,
-                                    "|>>",
-                                    Token::VerticalBarShiftRight,
-                                ),
-                                _ => self.start_binop_opt(chars, "|>", None),
+                            if self.dialect.supports_geometric_types() {
+                                if let Some('>') = chars.peek() {
+                                    return self.consume_for_binop(
+                                        chars,
+                                        "|>>",
+                                        Token::VerticalBarShiftRight,
+                                    );
+                                }
                             }
+                            Ok(Some(Token::VerticalBarRightAngleBracket))
                         }
                         // Bitshift '|' operator
                         _ => self.start_binop(chars, "|", Token::Pipe),