From 9c1e7ec681f134778d301baba5191c25ecce21bf Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 3 Oct 2026 08:44:02 +0200 Subject: [PATCH] PostgreSQL: Parse an empty SELECT target list without FROM --- src/dialect/postgresql.rs | 2 ++ src/parser/mod.rs | 27 ++++++++++++++++++++++++++- tests/sqlparser_common.rs | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 65bfd46a70..6f713e182e 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -285,6 +285,8 @@ impl Dialect for PostgreSqlDialect { /// ```sql /// SELECT from table_name /// ``` + /// + /// The target list is optional in fn supports_empty_projections(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 340b2d0779..a3d67b3cc0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15564,7 +15564,7 @@ impl<'a> Parser<'a> { } let projection = - if self.dialect.supports_empty_projections() && self.peek_keyword(Keyword::FROM) { + if self.dialect.supports_empty_projections() && self.peek_empty_projection() { vec![] } else { self.parse_projection()? @@ -15725,6 +15725,31 @@ impl<'a> Parser<'a> { }) } + /// Returns true if the next token ends a `SELECT` target list before it begins. + fn peek_empty_projection(&self) -> bool { + match &self.peek_token_ref().token { + Token::EOF | Token::SemiColon | Token::RParen => true, + Token::Word(w) => matches!( + w.keyword, + Keyword::INTO + | Keyword::FROM + | Keyword::WHERE + | Keyword::GROUP + | Keyword::HAVING + | Keyword::WINDOW + | Keyword::ORDER + | Keyword::LIMIT + | Keyword::OFFSET + | Keyword::FETCH + | Keyword::FOR + | Keyword::UNION + | Keyword::INTERSECT + | Keyword::EXCEPT + ), + _ => false, + } + } + /// Parses optimizer hints at the current token position. /// /// Collects all `/*prefix+...*/` and `--prefix+...` patterns. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1959bcd345..7be27ed9fc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16472,6 +16472,43 @@ fn parse_deeply_nested_boolean_expr_does_not_stackoverflow() { fn parse_select_without_projection() { let dialects = all_dialects_where(|d| d.supports_empty_projections()); dialects.verified_stmt("SELECT FROM users"); + for sql in [ + "SELECT", + "SELECT ALL", + "SELECT INTO t", + "SELECT WHERE true", + "SELECT GROUP BY () HAVING true", + "SELECT WINDOW w AS ()", + "SELECT ORDER BY 1", + "SELECT LIMIT 1", + "SELECT OFFSET 1", + "SELECT FETCH FIRST 1 ROWS ONLY", + "SELECT FOR UPDATE", + "SELECT UNION SELECT", + "SELECT INTERSECT SELECT", + "SELECT EXCEPT SELECT", + "SELECT EXISTS (SELECT)", + "SELECT * FROM (SELECT) AS t", + ] { + dialects.verified_stmt(sql); + } + assert_eq!( + dialects + .parse_sql_statements("SELECT; SELECT") + .unwrap() + .len(), + 2 + ); + assert_eq!( + dialects.parse_sql_statements("SELECT WHERE").unwrap_err(), + ParserError::ParserError("Expected: an expression, found: EOF".to_string()) + ); + + let dialects = all_dialects_where(|d| !d.supports_empty_projections()); + assert_eq!( + dialects.parse_sql_statements("SELECT").unwrap_err(), + ParserError::ParserError("Expected: an expression, found: EOF".to_string()) + ); } #[test]