Support remaining pipe operators (#1879)
Some checks are pending
license / Release Audit Tool (RAT) (push) Waiting to run
Rust / codestyle (push) Waiting to run
Rust / lint (push) Waiting to run
Rust / benchmark-lint (push) Waiting to run
Rust / compile (push) Waiting to run
Rust / docs (push) Waiting to run
Rust / compile-no-std (push) Waiting to run
Rust / test (beta) (push) Waiting to run
Rust / test (nightly) (push) Waiting to run
Rust / test (stable) (push) Waiting to run

This commit is contained in:
Simon Vandel Sillesen 2025-06-30 17:51:55 +02:00 committed by GitHub
parent 3bc94234df
commit abd80f9ecb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 739 additions and 0 deletions

View file

@ -2684,6 +2684,79 @@ pub enum PipeOperator {
/// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT)
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#tablesample_pipe_operator>
TableSample { 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: 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: SetQuantifier,
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: SetQuantifier,
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: SetQuantifier,
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 {
function: Function,
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: Vec<ExprWithAlias>,
value_column: Vec<Ident>,
value_source: PivotValueSource,
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 {
value_column: Ident,
name_column: Ident,
unpivot_columns: Vec<Ident>,
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 {
@ -2739,10 +2812,90 @@ impl fmt::Display for PipeOperator {
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, " ")?;
let parenthesized_queries: Vec<String> =
queries.iter().map(|query| format!("({query})")).collect();
write!(f, "{}", display_comma_separated(&parenthesized_queries))
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

View file

@ -9988,6 +9988,48 @@ 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 })
}
/// 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())
}
}
/// 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 {
@ -11134,6 +11176,19 @@ impl<'a> Parser<'a> {
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 => {
@ -11200,6 +11255,121 @@ impl<'a> Parser<'a> {
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_aliased_function_call)?;
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:?}"

View file

@ -15217,10 +15217,426 @@ fn parse_pipeline_operator() {
dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50 PERCENT)");
dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50) REPEATABLE (10)");
// rename pipe operator
dialects.verified_stmt("SELECT * FROM users |> RENAME old_name AS new_name");
dialects.verified_stmt("SELECT * FROM users |> RENAME id AS user_id, name AS user_name");
dialects.verified_query_with_canonical(
"SELECT * FROM users |> RENAME id user_id",
"SELECT * FROM users |> RENAME id AS user_id",
);
// union pipe operator
dialects.verified_stmt("SELECT * FROM users |> UNION ALL (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION (SELECT * FROM admins)");
// union pipe operator with multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> UNION ALL (SELECT * FROM admins), (SELECT * FROM guests)",
);
dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins), (SELECT * FROM guests), (SELECT * FROM employees)");
dialects.verified_stmt(
"SELECT * FROM users |> UNION (SELECT * FROM admins), (SELECT * FROM guests)",
);
// union pipe operator with BY NAME modifier
dialects.verified_stmt("SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION ALL BY NAME (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT BY NAME (SELECT * FROM admins)");
// union pipe operator with BY NAME and multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins), (SELECT * FROM guests)",
);
// intersect pipe operator (BigQuery requires DISTINCT modifier for INTERSECT)
dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins)");
// intersect pipe operator with BY NAME modifier
dialects
.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins)");
// intersect pipe operator with multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)",
);
// intersect pipe operator with BY NAME and multiple queries
dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)");
// except pipe operator (BigQuery requires DISTINCT modifier for EXCEPT)
dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins)");
// except pipe operator with BY NAME modifier
dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins)");
// except pipe operator with multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)",
);
// except pipe operator with BY NAME and multiple queries
dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)");
// call pipe operator
dialects.verified_stmt("SELECT * FROM users |> CALL my_function()");
dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5, 'test')");
dialects.verified_stmt(
"SELECT * FROM users |> CALL namespace.function_name(col1, col2, 'literal')",
);
// call pipe operator with complex arguments
dialects.verified_stmt("SELECT * FROM users |> CALL transform_data(col1 + col2)");
dialects.verified_stmt("SELECT * FROM users |> CALL analyze_data('param1', 100, true)");
// call pipe operator with aliases
dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) AS al");
dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5) AS result_table");
dialects.verified_stmt("SELECT * FROM users |> CALL namespace.func() AS my_alias");
// multiple call pipe operators in sequence
dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) |> CALL tvf2(arg2, arg3)");
dialects.verified_stmt(
"SELECT * FROM data |> CALL transform(col1) |> CALL validate() |> CALL process(param)",
);
// multiple call pipe operators with aliases
dialects.verified_stmt(
"SELECT * FROM input_table |> CALL tvf1(arg1) AS step1 |> CALL tvf2(arg2) AS step2",
);
dialects.verified_stmt(
"SELECT * FROM data |> CALL preprocess() AS clean_data |> CALL analyze(mode) AS results",
);
// call pipe operators mixed with other pipe operators
dialects.verified_stmt(
"SELECT * FROM users |> CALL transform() |> WHERE status = 'active' |> CALL process(param)",
);
dialects.verified_stmt(
"SELECT * FROM data |> CALL preprocess() AS clean |> SELECT col1, col2 |> CALL validate()",
);
// pivot pipe operator
dialects.verified_stmt(
"SELECT * FROM monthly_sales |> PIVOT(SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))",
);
dialects.verified_stmt("SELECT * FROM sales_data |> PIVOT(AVG(revenue) FOR region IN ('North', 'South', 'East', 'West'))");
// pivot pipe operator with multiple aggregate functions
dialects.verified_stmt("SELECT * FROM data |> PIVOT(SUM(sales) AS total_sales, COUNT(*) AS num_transactions FOR month IN ('Jan', 'Feb', 'Mar'))");
// pivot pipe operator with compound column names
dialects.verified_stmt("SELECT * FROM sales |> PIVOT(SUM(amount) FOR product.category IN ('Electronics', 'Clothing'))");
// pivot pipe operator mixed with other pipe operators
dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> PIVOT(SUM(revenue) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))");
// pivot pipe operator with aliases
dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales");
dialects.verified_stmt("SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category");
dialects.verified_stmt("SELECT * FROM sales |> PIVOT(COUNT(*) AS transactions, SUM(amount) AS total FOR region IN ('North', 'South')) AS regional_summary");
// pivot pipe operator with implicit aliases (without AS keyword)
dialects.verified_query_with_canonical(
"SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) quarterly_sales",
"SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales",
);
dialects.verified_query_with_canonical(
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) avg_by_category",
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category",
);
// unpivot pipe operator basic usage
dialects
.verified_stmt("SELECT * FROM sales |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))");
dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C))");
dialects.verified_stmt(
"SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory, disk))",
);
// unpivot pipe operator with multiple columns
dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (jan, feb, mar, apr, may, jun))");
dialects.verified_stmt(
"SELECT * FROM report |> UNPIVOT(score FOR subject IN (math, science, english, history))",
);
// unpivot pipe operator mixed with other pipe operators
dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))");
// unpivot pipe operator with aliases
dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales");
dialects.verified_stmt(
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data",
);
dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory)) AS metric_measurements");
// unpivot pipe operator with implicit aliases (without AS keyword)
dialects.verified_query_with_canonical(
"SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) unpivoted_sales",
"SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales",
);
dialects.verified_query_with_canonical(
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) transformed_data",
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data",
);
// many pipes
dialects.verified_stmt(
"SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC",
);
// join pipe operator - INNER JOIN
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt("SELECT * FROM users |> INNER JOIN orders ON users.id = orders.user_id");
// join pipe operator - LEFT JOIN
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt(
"SELECT * FROM users |> LEFT OUTER JOIN orders ON users.id = orders.user_id",
);
// join pipe operator - RIGHT JOIN
dialects.verified_stmt("SELECT * FROM users |> RIGHT JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt(
"SELECT * FROM users |> RIGHT OUTER JOIN orders ON users.id = orders.user_id",
);
// join pipe operator - FULL JOIN
dialects.verified_stmt("SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id");
dialects.verified_query_with_canonical(
"SELECT * FROM users |> FULL OUTER JOIN orders ON users.id = orders.user_id",
"SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id",
);
// join pipe operator - CROSS JOIN
dialects.verified_stmt("SELECT * FROM users |> CROSS JOIN orders");
// join pipe operator with USING
dialects.verified_query_with_canonical(
"SELECT * FROM users |> JOIN orders USING (user_id)",
"SELECT * FROM users |> JOIN orders USING(user_id)",
);
dialects.verified_query_with_canonical(
"SELECT * FROM users |> LEFT JOIN orders USING (user_id, order_date)",
"SELECT * FROM users |> LEFT JOIN orders USING(user_id, order_date)",
);
// join pipe operator with alias
dialects.verified_query_with_canonical(
"SELECT * FROM users |> JOIN orders o ON users.id = o.user_id",
"SELECT * FROM users |> JOIN orders AS o ON users.id = o.user_id",
);
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders AS o ON users.id = o.user_id");
// join pipe operator with complex ON condition
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id AND orders.status = 'active'");
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id AND orders.amount > 100");
// multiple join pipe operators
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> JOIN products ON orders.product_id = products.id");
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id |> RIGHT JOIN products ON orders.product_id = products.id");
// join pipe operator with other pipe operators
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> WHERE orders.amount > 100");
dialects.verified_stmt("SELECT * FROM users |> WHERE users.active = true |> LEFT JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> SELECT users.name, orders.amount");
}
#[test]
fn parse_pipeline_operator_negative_tests() {
let dialects = all_dialects_where(|d| d.supports_pipe_operator());
// Test that plain EXCEPT without DISTINCT fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> EXCEPT (SELECT * FROM admins)")
.unwrap_err()
);
// Test that EXCEPT ALL fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> EXCEPT ALL (SELECT * FROM admins)")
.unwrap_err()
);
// Test that EXCEPT BY NAME without DISTINCT fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> EXCEPT BY NAME (SELECT * FROM admins)")
.unwrap_err()
);
// Test that EXCEPT ALL BY NAME fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements(
"SELECT * FROM users |> EXCEPT ALL BY NAME (SELECT * FROM admins)"
)
.unwrap_err()
);
// Test that plain INTERSECT without DISTINCT fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> INTERSECT (SELECT * FROM admins)")
.unwrap_err()
);
// Test that INTERSECT ALL fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> INTERSECT ALL (SELECT * FROM admins)")
.unwrap_err()
);
// Test that INTERSECT BY NAME without DISTINCT fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> INTERSECT BY NAME (SELECT * FROM admins)")
.unwrap_err()
);
// Test that INTERSECT ALL BY NAME fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements(
"SELECT * FROM users |> INTERSECT ALL BY NAME (SELECT * FROM admins)"
)
.unwrap_err()
);
// Test that CALL without function name fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL")
.is_err());
// Test that CALL without parentheses fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL my_function")
.is_err());
// Test that CALL with invalid function syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL 123invalid")
.is_err());
// Test that CALL with malformed arguments fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL my_function(,)")
.is_err());
// Test that CALL with invalid alias syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL my_function() AS")
.is_err());
// Test that PIVOT without parentheses fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT SUM(amount) FOR month IN ('Jan')")
.is_err());
// Test that PIVOT without FOR keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) month IN ('Jan'))")
.is_err());
// Test that PIVOT without IN keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month ('Jan'))")
.is_err());
// Test that PIVOT with empty IN list fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ())")
.is_err());
// Test that PIVOT with invalid alias syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS")
.is_err());
// Test UNPIVOT negative cases
// Test that UNPIVOT without parentheses fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT value FOR name IN col1, col2")
.is_err());
// Test that UNPIVOT without FOR keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value name IN (col1, col2))")
.is_err());
// Test that UNPIVOT without IN keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name (col1, col2))")
.is_err());
// Test that UNPIVOT with missing value column fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(FOR name IN (col1, col2))")
.is_err());
// Test that UNPIVOT with missing name column fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR IN (col1, col2))")
.is_err());
// Test that UNPIVOT with empty IN list fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN ())")
.is_err());
// Test that UNPIVOT with invalid alias syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)) AS")
.is_err());
// Test that UNPIVOT with missing closing parenthesis fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)")
.is_err());
// Test that JOIN without table name fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN ON users.id = orders.user_id")
.is_err());
// Test that CROSS JOIN with ON condition fails
assert!(dialects
.parse_sql_statements(
"SELECT * FROM users |> CROSS JOIN orders ON users.id = orders.user_id"
)
.is_err());
// Test that CROSS JOIN with USING condition fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CROSS JOIN orders USING (user_id)")
.is_err());
// Test that JOIN with empty USING list fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN orders USING ()")
.is_err());
// Test that JOIN with malformed ON condition fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN orders ON")
.is_err());
// Test that JOIN with invalid USING syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN orders USING user_id")
.is_err());
}
#[test]