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
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),