Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 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,13 @@ 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] =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe at least these other ones are missing, I am not sure which of these others should be included: https://github.com/postgres/postgres/blob/master/src/include/parser/kwlist.h

&[
    Keyword::ANALYZE,
    Keyword::CLUSTER,
    Keyword::END,
    Keyword::EXCLUDE,
    Keyword::EXPLAIN,
    Keyword::LATERAL,
    Keyword::SELECT,
    Keyword::VALUES,
    Keyword::VIEW,
]

&[Keyword::SELECT, Keyword::ANALYZE, Keyword::LATERAL];

const PERIOD_PREC: u8 = 200;
const DOUBLE_COLON_PREC: u8 = 140;
const BRACKET_PREC: u8 = 130;
Expand Down Expand Up @@ -368,4 +375,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)
}
}
34 changes: 29 additions & 5 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1422,11 +1422,19 @@ 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`.
if !self.in_column_definition_state() && self.peek_keyword(Keyword::COLLATE) {
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 @@ -1444,6 +1452,22 @@ impl<'a> Parser<'a> {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shared-parser change also enables these aliases for unrelated dialects. I am not sure about how to best handle excessive permissiveness.

For example, BigQueryDialect considers AND a column alias under its current predicate, so SELECT 1 AND now I believe would succeed instead of reporting the missing right-hand operand.

GoogleSQL explicitly requires reserved keywords such as AND, OR, and COLLATE to be quoted when used as identifiers.

Let's try to find a clean way to handle these cases and add negative non-PostgreSQL tests.


// `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`.
if let Token::Word(w) = &self.peek_token_ref().token {
let kw = w.keyword;
if matches!(kw, Keyword::AND | Keyword::OR | Keyword::COLLATE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgreSQL also permits these bare aliases before the following query clauses. For example, SELECT 1 and FROM t and SELECT 1 collate FROM t are valid, but FROM is not among these four lookahead tokens, so the parser still treats the alias as an operator and fails.

Please handle clause boundaries such as FROM, INTO, WHERE, and ORDER BY, and add tests with aliases followed by a clause. I may have missed some others, I may not recall all of them.

&& matches!(
self.peek_nth_token_ref(1).token,
Token::EOF | Token::Comma | Token::RParen | Token::SemiColon
)
&& self.dialect.is_column_alias(&kw, self)
{
break;
}
}

expr = self.parse_infix(expr, next_precedence)?;
}
Ok(expr)
Expand Down
36 changes: 25 additions & 11 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
19 changes: 19 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9918,3 +9918,22 @@ fn parse_non_reserved_keywords_as_table_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>
pg().verified_stmt("SELECT 1 AS select");
for kw in ["select", "analyze", "lateral", "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""#);

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