PostgreSQL: Parse an empty SELECT target list without FROM
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 65bfd46..6f713e1 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs
@@ -285,6 +285,8 @@ /// ```sql /// SELECT from table_name /// ``` + /// + /// The target list is optional in <https://www.postgresql.org/docs/current/sql-select.html> fn supports_empty_projections(&self) -> bool { true }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 340b2d0..a3d67b3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs
@@ -15564,7 +15564,7 @@ } 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 @@ }) } + /// 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 1959bcd..7be27ed 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs
@@ -16472,6 +16472,43 @@ 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]