Skip to content
5 changes: 5 additions & 0 deletions src/dialect/bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#reserved_keywords>
Keyword::AND,
Keyword::OR,
Keyword::COLLATE,
];

/// A [`Dialect`] for [Google Bigquery](https://cloud.google.com/bigquery/)
Expand Down
36 changes: 34 additions & 2 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>.
Expand All @@ -54,6 +54,30 @@ 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 <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>
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,
Comment thread
BenSatori marked this conversation as resolved.
// These are not PostgreSQL keywords, but the parser treats them as clause keywords.
Keyword::DISTRIBUTE,
Keyword::MINUS,
Keyword::SORT,
Keyword::TOP,
];

const PERIOD_PREC: u8 = 200;
const DOUBLE_COLON_PREC: u8 = 140;
const BRACKET_PREC: u8 = 130;
Expand Down Expand Up @@ -368,4 +392,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 <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>
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)
}
}
4 changes: 3 additions & 1 deletion src/dialect/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,14 @@ impl Dialect for SnowflakeDialect {
// Reserved keywords by the Snowflake dialect, which seem to be less strictive
// than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following
// keywords were tested with the this statement: `SELECT 1 <KW>`.
Keyword::FROM
Keyword::AND
| Keyword::FROM
| Keyword::GROUP
| Keyword::HAVING
| Keyword::INTERSECT
| Keyword::INTO
| Keyword::MINUS
| Keyword::OR
| Keyword::ORDER
| Keyword::SELECT
| Keyword::UNION
Expand Down
5 changes: 5 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,11 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
Keyword::DISTRIBUTE,
Keyword::RETURNING,
Keyword::VALUES,
// These are also operators, so `SELECT <expr> alias` would otherwise be parsed
// as `SELECT <expr> <operator> <right-hand side>`:
Keyword::AND,
Keyword::OR,
Keyword::COLLATE,
// Reserved only as a column alias in the `SELECT` clause
Keyword::FROM,
Keyword::INTO,
Expand Down
49 changes: 44 additions & 5 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1424,11 +1424,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:?}");
Expand All @@ -1446,11 +1457,39 @@ impl<'a> Parser<'a> {
break;
}
Comment thread
BenSatori marked this conversation as resolved.

// `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)
Comment thread
BenSatori marked this conversation as resolved.
&& 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<Statement, ParserError> {
let condition = self.parse_expr()?;
Expand Down
44 changes: 33 additions & 11 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8357,38 +8357,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!(
Expand Down Expand Up @@ -20222,6 +20236,14 @@ fn parse_alter_table_column_position() {
}
}

#[test]
fn parse_and_or_not_bare_column_alias() {
let dialects = all_dialects_but_pg();
for sql in ["SELECT 1 AND", "SELECT 1 OR", "SELECT 1 AND, 2"] {
assert!(dialects.parse_sql_statements(sql).is_err(), "{sql}");
}
}

#[test]
fn parse_placeholder_disallows_quoted_ident() {
let dialects = TestedDialects::new(vec![
Expand Down
48 changes: 48 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10068,6 +10068,54 @@ fn parse_insert_by_name_keywords_as_table_and_alias() {
}
}

#[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 <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>
for kw in [
"analyze",
"cluster",
"end",
"exclude",
"explain",
"lateral",
"select",
"values",
"view",
"and",
"or",
"collate",
"distribute",
"minus",
"sort",
"top",
] {
pg().one_statement_parses_to(
&format!("SELECT a {kw} FROM tbl_name"),
&format!("SELECT a AS {kw} FROM tbl_name"),
);
}

pg().one_statement_parses_to(
"SELECT (SELECT c FROM tbl_name LIMIT 1) select",
"SELECT (SELECT c FROM tbl_name LIMIT 1) AS select",
);
for kw in ["and", "or", "collate"] {
pg().one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}"));
}

// `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"#);
Comment thread
BenSatori marked this conversation as resolved.
assert!(pg().parse_sql_statements("SELECT 1 COLLATE 2").is_err());
pg().verified_stmt("SELECT a AND sort FROM tbl_name WHERE b OR top");

// Keywords that require `AS` still cannot be used as a bare column alias.
assert!(pg().parse_sql_statements("SELECT 1 where").is_err());
}

#[test]
fn parse_pg_abs_space_before_negative_operand() {
// `@-` tokenizes as a geometric operator prefix, so displaying PGAbs
Expand Down
Loading