diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index d34be9a40..49eac6512 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -39,6 +39,11 @@ const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::FROM, Keyword::INTO, Keyword::END, + // GoogleSQL requires reserved keywords to be quoted when used as identifiers. + // See + Keyword::AND, + Keyword::OR, + Keyword::COLLATE, ]; /// A [`Dialect`] for [Google Bigquery](https://cloud.google.com/bigquery/) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 3bec6ceba..b8667b262 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -29,11 +29,11 @@ use log::debug; use crate::dialect::{Dialect, Precedence}; -use crate::keywords::Keyword; +use crate::keywords::{self, Keyword}; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; -use super::keywords::{self, RESERVED_FOR_IDENTIFIER}; +use super::keywords::RESERVED_FOR_IDENTIFIER; /// Keywords in [`keywords::RESERVED_FOR_TABLE_ALIAS`] because of other dialects, yet are safe for aliasing in PostgreSQL. /// See . @@ -54,6 +54,25 @@ const RESERVED_EXCLUSIONS_FOR_TABLE_ALIAS: &[Keyword] = &[ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PostgreSqlDialect {} +/// Keywords that PostgreSQL additionally allows (on top of +/// [keywords::RESERVED_FOR_COLUMN_ALIAS]) to be used as a bare (`AS`-less) +/// column alias. +/// See +const ADDITIONALLY_ALLOWED_BARE_COLUMN_ALIASES: &[Keyword] = &[ + Keyword::SELECT, + Keyword::ANALYZE, + Keyword::LATERAL, + Keyword::AND, + Keyword::OR, + Keyword::COLLATE, + Keyword::CLUSTER, + Keyword::END, + Keyword::EXCLUDE, + Keyword::EXPLAIN, + Keyword::VALUES, + Keyword::VIEW, +]; + const PERIOD_PREC: u8 = 200; const DOUBLE_COLON_PREC: u8 = 140; const BRACKET_PREC: u8 = 130; @@ -368,4 +387,12 @@ impl Dialect for PostgreSqlDialect { fn supports_comment_optimizer_hint(&self) -> bool { true } + + /// Even reserved keywords can be used as a bare (`AS`-less) column alias in + /// PostgreSQL, unless they are in a small set of keywords that require `AS`. + /// See + fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + ADDITIONALLY_ALLOWED_BARE_COLUMN_ALIASES.contains(kw) + || !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) + } } diff --git a/src/keywords.rs b/src/keywords.rs index 7b91eecba..1576a9bcf 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1293,6 +1293,11 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::DISTRIBUTE, Keyword::RETURNING, Keyword::VALUES, + // These are also operators, so `SELECT alias` would otherwise be parsed + // as `SELECT `: + Keyword::AND, + Keyword::OR, + Keyword::COLLATE, // Reserved only as a column alias in the `SELECT` clause Keyword::FROM, Keyword::INTO, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 15f135fff..3174fff62 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1422,11 +1422,22 @@ impl<'a> Parser<'a> { // Parse an optional collation cast operator following `expr`. // // For example (MSSQL): t1.a COLLATE Latin1_General_CI_AS - if !self.in_column_definition_state() && self.parse_keyword(Keyword::COLLATE) { - expr = Expr::Collate { - expr: Box::new(expr), - collation: self.parse_object_name(false)?, - }; + // + // `COLLATE` with no collation name following it is not a cast operator; it's a bare + // (`AS`-less) column alias, e.g. Postgres' `SELECT 1 collate` or `SELECT 1 collate FROM t`. + if !self.in_column_definition_state() + && self.peek_keyword(Keyword::COLLATE) + && self.next_token_starts_an_expr() + { + if let Some(collation) = self.maybe_parse(|parser| { + parser.expect_keyword(Keyword::COLLATE)?; + parser.parse_object_name(false) + })? { + expr = Expr::Collate { + expr: Box::new(expr), + collation, + }; + } } debug!("prefix: {expr:?}"); @@ -1444,11 +1455,39 @@ impl<'a> Parser<'a> { break; } + // `AND`/`OR`/`COLLATE` with no right-hand expression following it is not a + // binary operator or collation cast; it's a bare (`AS`-less) column alias, + // e.g. Postgres' `SELECT 1 and` or `SELECT 1 collate FROM t`. + if let Token::Word(w) = &self.peek_token_ref().token { + let kw = w.keyword; + if matches!(kw, Keyword::AND | Keyword::OR | Keyword::COLLATE) + && self.dialect.is_column_alias(&kw, self) + && !self.next_token_starts_an_expr() + { + break; + } + } + expr = self.parse_infix(expr, next_precedence)?; } Ok(expr) } + /// Returns false if the token following the current one cannot be the start of an + /// expression: either a token that never starts an expression (e.g. `,`, `)`, `;`, EOF) + /// or a keyword that the dialect reserves for a query clause (and thus requires `AS` + /// to be used as a column alias, see [`Dialect::is_column_alias`]). + fn next_token_starts_an_expr(&mut self) -> bool { + match &self.peek_nth_token_ref(1).token { + Token::EOF | Token::Comma | Token::RParen | Token::SemiColon => false, + Token::Word(w) => { + let kw = w.keyword; + kw == Keyword::NoKeyword || self.dialect.is_column_alias(&kw, self) + } + _ => true, + } + } + /// Parse `ASSERT` statement. pub fn parse_assert(&mut self) -> Result { let condition = self.parse_expr()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2de6062b2..6808e7148 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8354,38 +8354,52 @@ fn parse_values() { #[test] fn parse_multiple_statements() { - fn test_with(sql1: &str, sql2_kw: &str, sql2_rest: &str) { + fn test_with(dialects: &TestedDialects, sql1: &str, sql2_kw: &str, sql2_rest: &str) { // Check that a string consisting of two statements delimited by a semicolon // parses the same as both statements individually: - let res = parse_sql_statements(&(sql1.to_owned() + ";" + sql2_kw + sql2_rest)); + let res = dialects.parse_sql_statements(&(sql1.to_owned() + ";" + sql2_kw + sql2_rest)); assert_eq!( vec![ - one_statement_parses_to(sql1, ""), - one_statement_parses_to(&(sql2_kw.to_owned() + sql2_rest), ""), + dialects.one_statement_parses_to(sql1, ""), + dialects.one_statement_parses_to(&(sql2_kw.to_owned() + sql2_rest), ""), ], res.unwrap() ); // Check that extra semicolon at the end is stripped by normalization: - one_statement_parses_to(&(sql1.to_owned() + ";"), sql1); + dialects.one_statement_parses_to(&(sql1.to_owned() + ";"), sql1); // Check that forgetting the semicolon results in an error: - let res = parse_sql_statements(&(sql1.to_owned() + " " + sql2_kw + sql2_rest)); + let res = dialects.parse_sql_statements(&(sql1.to_owned() + " " + sql2_kw + sql2_rest)); assert_eq!( ParserError::ParserError("Expected: end of statement, found: ".to_string() + sql2_kw), res.unwrap_err() ); } - test_with("SELECT foo", "SELECT", " bar"); + // PostgreSQL allows a bare `SELECT` to be used as a column alias, so unlike + // the other dialects, omitting the semicolon here does not result in an + // error there. + test_with(&all_dialects_but_pg(), "SELECT foo", "SELECT", " bar"); // ensure that SELECT/WITH is not parsed as a table or column alias if ';' // separating the statements is omitted: - test_with("SELECT foo FROM baz", "SELECT", " bar"); - test_with("SELECT foo", "WITH", " cte AS (SELECT 1 AS s) SELECT bar"); + test_with(&all_dialects(), "SELECT foo FROM baz", "SELECT", " bar"); test_with( + &all_dialects(), + "SELECT foo", + "WITH", + " cte AS (SELECT 1 AS s) SELECT bar", + ); + test_with( + &all_dialects(), "SELECT foo FROM baz", "WITH", " cte AS (SELECT 1 AS s) SELECT bar", ); - test_with("DELETE FROM foo", "SELECT", " bar"); - test_with("INSERT INTO foo VALUES (1)", "SELECT", " bar"); + test_with(&all_dialects(), "DELETE FROM foo", "SELECT", " bar"); + test_with( + &all_dialects(), + "INSERT INTO foo VALUES (1)", + "SELECT", + " bar", + ); // Since MySQL supports the `CREATE TABLE SELECT` syntax, this needs to be handled separately let res = parse_sql_statements("CREATE TABLE foo (baz INT); SELECT bar"); assert_eq!( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d71e49b27..ea3760d34 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9953,3 +9953,27 @@ fn parse_insert_by_name_keywords_as_table_and_alias() { statement => panic!("Expected INSERT statement, got: {statement:?}"), } } + +#[test] +fn parse_reserved_keyword_as_bare_column_alias() { + // PostgreSQL allows (almost) any keyword, reserved or not, to be used as a bare + // (`AS`-less) column alias; only a small set of keywords require a leading `AS`. + // See + for kw in [ + "analyze", "cluster", "end", "exclude", "explain", "lateral", "select", "values", "view", + "and", "or", "collate", + ] { + pg().one_statement_parses_to( + &format!("SELECT a {kw} FROM tbl_name"), + &format!("SELECT a AS {kw} FROM tbl_name"), + ); + } + + // `AND`/`OR`/`COLLATE` are still parsed as operators when followed by an operand. + pg().verified_stmt("SELECT 1 AND 2"); + pg().verified_stmt("SELECT 1 OR 2"); + pg().verified_stmt(r#"SELECT 1 COLLATE "de_DE" FROM tbl_name"#); + + // Keywords that require `AS` still cannot be used as a bare column alias. + assert!(pg().parse_sql_statements("SELECT 1 where").is_err()); +}