You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by lu...@apache.org on 2026/10/08 19:44:19 UTC
(datafusion-sqlparser-rs) branch upstream/reserved-keywords-not-identifiers updated (d80dd0b5 -> 01ac60f2)
This is an automated email from the ASF dual-hosted git repository.
LucaCappelletti94 pushed a change to branch upstream/reserved-keywords-not-identifiers
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
discard d80dd0b5 Postgres/SQLite: reject reserved keywords as bare expression identifiers
add 14cbf755 SQLite: Support identifier characters above U+FFFF (#2555)
new 01ac60f2 Postgres/SQLite: reject reserved keywords as bare expression identifiers
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (d80dd0b5)
\
N -- N -- N refs/heads/upstream/reserved-keywords-not-identifiers (01ac60f2)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
src/dialect/sqlite.rs | 5 +----
tests/sqlparser_sqlite.rs | 13 +++++++++++++
2 files changed, 14 insertions(+), 4 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org
(datafusion-sqlparser-rs) 01/01: Postgres/SQLite: reject reserved keywords as bare expression identifiers
Posted by lu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
LucaCappelletti94 pushed a commit to branch upstream/reserved-keywords-not-identifiers
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit 01ac60f2036ea36197c0a0023d4b1b8e00bfa465
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Fri Oct 2 21:08:17 2026 +0200
Postgres/SQLite: reject reserved keywords as bare expression identifiers
A keyword whose special expression form failed to parse, or that has
no special form at all (TO, ON, SELECT, ...), fell back to being
parsed as a plain identifier regardless of dialect. PostgreSqlDialect
and SQLiteDialect now consult their own reserved-keyword sets, built
from PostgreSQL's pg_get_keywords() catalog and SQLite's own keyword
list, so a bare reserved keyword is rejected the way both engines
reject it, while keywords that remain valid functions (RIGHT, LEFT)
or bare columns (SET in PostgreSQL) keep working.
---
src/dialect/mod.rs | 5 ++
src/dialect/postgresql.rs | 116 +++++++++++++++++++++++++++++++++++++++++---
src/dialect/sqlite.rs | 71 +++++++++++++++++++++++++++
src/parser/mod.rs | 18 ++++++-
tests/sqlparser_common.rs | 37 +++++++++-----
tests/sqlparser_postgres.rs | 28 +++++++++++
tests/sqlparser_sqlite.rs | 29 +++++++++++
7 files changed, 285 insertions(+), 19 deletions(-)
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 2beb5655..d4c4d1d4 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -1406,6 +1406,11 @@ pub trait Dialect: Debug + Any {
keywords::RESERVED_FOR_IDENTIFIER.contains(&kw)
}
+ /// Returns true if the keyword can never be a bare identifier, even one with no dedicated expression form to fall back from (e.g. `TO`, `ON`).
+ fn disallows_bare_identifier(&self, _kw: Keyword) -> bool {
+ false
+ }
+
/// Returns reserved keywords that may prefix a select item expression
/// e.g. `SELECT CONNECT_BY_ROOT name FROM Tbl2` (Snowflake)
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs
index 3bec6ceb..741c13ad 100644
--- a/src/dialect/postgresql.rs
+++ b/src/dialect/postgresql.rs
@@ -33,7 +33,110 @@ use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
use crate::tokenizer::Token;
-use super::keywords::{self, RESERVED_FOR_IDENTIFIER};
+use super::keywords;
+
+/// Keywords PostgreSQL rejects as a bare expression identifier (catcode `R` or `T`), though the `T` subset may still appear as a function or type name.
+/// See <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>.
+const PG_RESERVED_FOR_IDENTIFIER: &[Keyword] = &[
+ Keyword::ALL,
+ Keyword::ANALYZE,
+ Keyword::AND,
+ Keyword::ANY,
+ Keyword::ARRAY,
+ Keyword::AS,
+ Keyword::ASC,
+ Keyword::ASYMMETRIC,
+ Keyword::AUTHORIZATION,
+ Keyword::BINARY,
+ Keyword::BOTH,
+ Keyword::CASE,
+ Keyword::CAST,
+ Keyword::CHECK,
+ Keyword::COLLATE,
+ Keyword::COLLATION,
+ Keyword::COLUMN,
+ Keyword::CONCURRENTLY,
+ Keyword::CONSTRAINT,
+ Keyword::CREATE,
+ Keyword::CROSS,
+ Keyword::CURRENT_CATALOG,
+ Keyword::CURRENT_DATE,
+ Keyword::CURRENT_ROLE,
+ Keyword::CURRENT_SCHEMA,
+ Keyword::CURRENT_TIME,
+ Keyword::CURRENT_TIMESTAMP,
+ Keyword::CURRENT_USER,
+ Keyword::DEFERRABLE,
+ Keyword::DESC,
+ Keyword::DISTINCT,
+ Keyword::DO,
+ Keyword::ELSE,
+ Keyword::END,
+ Keyword::EXCEPT,
+ Keyword::FALSE,
+ Keyword::FETCH,
+ Keyword::FOR,
+ Keyword::FOREIGN,
+ Keyword::FREEZE,
+ Keyword::FROM,
+ Keyword::FULL,
+ Keyword::GRANT,
+ Keyword::GROUP,
+ Keyword::HAVING,
+ Keyword::ILIKE,
+ Keyword::IN,
+ Keyword::INITIALLY,
+ Keyword::INNER,
+ Keyword::INTERSECT,
+ Keyword::INTO,
+ Keyword::IS,
+ Keyword::JOIN,
+ Keyword::LATERAL,
+ Keyword::LEADING,
+ Keyword::LEFT,
+ Keyword::LIKE,
+ Keyword::LIMIT,
+ Keyword::LOCALTIME,
+ Keyword::LOCALTIMESTAMP,
+ Keyword::NATURAL,
+ Keyword::NOT,
+ Keyword::NOTNULL,
+ Keyword::NULL,
+ Keyword::OFFSET,
+ Keyword::ON,
+ Keyword::ONLY,
+ Keyword::OR,
+ Keyword::ORDER,
+ Keyword::OUTER,
+ Keyword::OVERLAPS,
+ Keyword::PLACING,
+ Keyword::PRIMARY,
+ Keyword::REFERENCES,
+ Keyword::RETURNING,
+ Keyword::RIGHT,
+ Keyword::SELECT,
+ Keyword::SESSION_USER,
+ Keyword::SIMILAR,
+ Keyword::SOME,
+ Keyword::SYMMETRIC,
+ Keyword::SYSTEM_USER,
+ Keyword::TABLE,
+ Keyword::TABLESAMPLE,
+ Keyword::THEN,
+ Keyword::TO,
+ Keyword::TRAILING,
+ Keyword::TRUE,
+ Keyword::UNION,
+ Keyword::UNIQUE,
+ Keyword::USER,
+ Keyword::USING,
+ Keyword::VARIADIC,
+ Keyword::VERBOSE,
+ Keyword::WHEN,
+ Keyword::WHERE,
+ Keyword::WINDOW,
+ Keyword::WITH,
+];
/// 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>.
@@ -98,11 +201,12 @@ impl Dialect for PostgreSqlDialect {
}
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
- if matches!(kw, Keyword::INTERVAL) {
- false
- } else {
- RESERVED_FOR_IDENTIFIER.contains(&kw)
- }
+ // EXISTS(...) is always PostgreSQL's subquery form, never a function call.
+ kw == Keyword::EXISTS || PG_RESERVED_FOR_IDENTIFIER.contains(&kw)
+ }
+
+ fn disallows_bare_identifier(&self, kw: Keyword) -> bool {
+ PG_RESERVED_FOR_IDENTIFIER.contains(&kw)
}
fn is_table_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs
index 6f9ec0d2..98d40fee 100644
--- a/src/dialect/sqlite.rs
+++ b/src/dialect/sqlite.rs
@@ -24,6 +24,69 @@ use crate::dialect::Dialect;
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
+/// Keywords SQLite rejects as a bare expression identifier or function name, unlike most SQLite keywords.
+/// See <https://www.sqlite.org/lang_keywords.html>.
+const SQLITE_RESERVED_FOR_IDENTIFIER: &[Keyword] = &[
+ Keyword::ADD,
+ Keyword::ALL,
+ Keyword::ALTER,
+ Keyword::AND,
+ Keyword::AS,
+ Keyword::AUTOINCREMENT,
+ Keyword::BETWEEN,
+ Keyword::CASE,
+ Keyword::CAST,
+ Keyword::CHECK,
+ Keyword::COLLATE,
+ Keyword::COMMIT,
+ Keyword::CONSTRAINT,
+ Keyword::CREATE,
+ Keyword::DEFAULT,
+ Keyword::DEFERRABLE,
+ Keyword::DELETE,
+ Keyword::DISTINCT,
+ Keyword::DROP,
+ Keyword::ELSE,
+ Keyword::ESCAPE,
+ Keyword::EXCEPT,
+ Keyword::EXISTS,
+ Keyword::FOREIGN,
+ Keyword::FROM,
+ Keyword::GROUP,
+ Keyword::HAVING,
+ Keyword::IN,
+ Keyword::INDEX,
+ Keyword::INSERT,
+ Keyword::INTERSECT,
+ Keyword::INTO,
+ Keyword::IS,
+ Keyword::JOIN,
+ Keyword::LIMIT,
+ Keyword::NOT,
+ Keyword::NOTHING,
+ Keyword::NOTNULL,
+ Keyword::ON,
+ Keyword::OR,
+ Keyword::ORDER,
+ Keyword::PRIMARY,
+ Keyword::RAISE,
+ Keyword::REFERENCES,
+ Keyword::RETURNING,
+ Keyword::SELECT,
+ Keyword::SET,
+ Keyword::TABLE,
+ Keyword::THEN,
+ Keyword::TO,
+ Keyword::TRANSACTION,
+ Keyword::UNION,
+ Keyword::UNIQUE,
+ Keyword::UPDATE,
+ Keyword::USING,
+ Keyword::VALUES,
+ Keyword::WHEN,
+ Keyword::WHERE,
+];
+
/// A [`Dialect`] for [SQLite](https://www.sqlite.org)
///
/// This dialect allows columns in a
@@ -142,4 +205,12 @@ impl Dialect for SQLiteDialect {
fn supports_national_string_literal(&self) -> bool {
false
}
+
+ fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
+ SQLITE_RESERVED_FOR_IDENTIFIER.contains(&kw)
+ }
+
+ fn disallows_bare_identifier(&self, kw: Keyword) -> bool {
+ SQLITE_RESERVED_FOR_IDENTIFIER.contains(&kw)
+ }
}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..b83984f7 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1739,7 +1739,16 @@ impl<'a> Parser<'a> {
syntax: LambdaSyntax::Arrow,
}))
}
- _ => Ok(Expr::Identifier(w.to_ident(w_span))),
+ _ => {
+ if self.dialect.disallows_bare_identifier(w.keyword) {
+ parser_err!(
+ format!("Expected an expression, found: {}", w.value),
+ w_span.start
+ )
+ } else {
+ Ok(Expr::Identifier(w.to_ident(w_span)))
+ }
+ }
}
}
@@ -2388,7 +2397,12 @@ impl<'a> Parser<'a> {
if next_token == &Token::Comma || next_token == &Token::RParen {
return Ok(UtilityOption { name, arg: None });
}
- let arg = self.parse_expr()?;
+ // PostgreSQL's `generic_option_arg` grammar special-cases `ON` (e.g. `EXPLAIN (ANALYZE ON)`).
+ let arg = if self.peek_keyword(Keyword::ON) {
+ Expr::Identifier(self.parse_identifier()?)
+ } else {
+ self.parse_expr()?
+ };
Ok(UtilityOption {
name,
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 8e08c657..ef406283 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -1207,10 +1207,10 @@ fn parse_select_wildcard() {
#[test]
fn parse_count_wildcard() {
- verified_only_select("SELECT COUNT(*) FROM Order WHERE id = 10");
+ verified_only_select("SELECT COUNT(*) FROM orders WHERE id = 10");
verified_only_select(
- "SELECT COUNT(Employee.*) FROM Order JOIN Employee ON Order.employee = Employee.id",
+ "SELECT COUNT(Employee.*) FROM orders JOIN Employee ON orders.employee = Employee.id",
);
}
@@ -2990,7 +2990,9 @@ fn parse_select_order_by_all() {
#[test]
fn parse_select_order_by_not_support_all() {
fn chk(sql: &str, except_order_by: OrderByKind) {
- let dialects = all_dialects_where(|d| !d.supports_order_by_all());
+ let dialects = all_dialects_where(|d| {
+ !d.supports_order_by_all() && !d.is::<PostgreSqlDialect>() && !d.is::<SQLiteDialect>()
+ });
let select = dialects.verified_query(sql);
assert_eq!(
except_order_by,
@@ -3157,13 +3159,13 @@ fn parse_select_group_by() {
#[test]
fn parse_select_group_by_all() {
- let sql = "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL";
+ let sql = "SELECT id, fname, lname, SUM(amount) FROM customer GROUP BY ALL";
let select = verified_only_select(sql);
assert_eq!(GroupByExpr::All(vec![]), select.group_by);
one_statement_parses_to(
- "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL",
- "SELECT id, fname, lname, SUM(order) FROM customer GROUP BY ALL",
+ "SELECT id, fname, lname, SUM(amount) FROM customer GROUP BY ALL",
+ "SELECT id, fname, lname, SUM(amount) FROM customer GROUP BY ALL",
);
}
@@ -9146,11 +9148,24 @@ fn parse_create_warehouse() {
#[test]
fn parse_invalid_subquery_without_parens() {
- let res = parse_sql_statements("SELECT SELECT 1 FROM bar WHERE 1=1 FROM baz");
+ let sql = "SELECT SELECT 1 FROM bar WHERE 1=1 FROM baz";
+
+ let res = all_dialects_except(|d| d.is::<PostgreSqlDialect>() || d.is::<SQLiteDialect>())
+ .parse_sql_statements(sql);
assert_eq!(
ParserError::ParserError("Expected: end of statement, found: 1".to_string()),
res.unwrap_err()
);
+
+ let res = TestedDialects::new(vec![
+ Box::new(PostgreSqlDialect {}),
+ Box::new(SQLiteDialect {}),
+ ])
+ .parse_sql_statements(sql);
+ assert_eq!(
+ ParserError::ParserError("Expected an expression, found: SELECT".to_string()),
+ res.unwrap_err()
+ );
}
#[test]
@@ -14888,7 +14903,7 @@ fn test_create_policy() {
"CREATE POLICY my_policy ON my_table ",
"AS PERMISSIVE FOR SELECT ",
"TO my_role, CURRENT_USER ",
- "USING (c0 IN (SELECT column FROM t0)) ",
+ "USING (c0 IN (SELECT col FROM t0)) ",
"WITH CHECK (1 = 1)"
));
// omit AS / FOR / TO / USING / WITH CHECK clauses is allowed
@@ -15311,7 +15326,7 @@ fn parse_method_select() {
"SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T",
);
let _ = verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T");
- let _ = verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T");
+ let _ = verified_only_select("SELECT CAST(col AS XML).value('.', 'NVARCHAR(MAX)') AS T");
// `CONVERT` support
let dialects =
@@ -15350,7 +15365,7 @@ fn parse_method_expr() {
}
_ => unreachable!(),
}
- let expr = verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')");
+ let expr = verified_expr("CAST(col AS XML).value('.', 'NVARCHAR(MAX)')");
match expr {
Expr::CompoundFieldAccess { root, access_chain } => {
assert!(matches!(*root, Expr::Cast { .. }));
@@ -16682,7 +16697,7 @@ fn parse_raise_statement() {
verified_stmt("RAISE USING MESSAGE = 'error'");
verified_stmt("RAISE myerror");
verified_stmt("RAISE 42");
- verified_stmt("RAISE using");
+ verified_stmt("RAISE custom_error");
verified_stmt("RAISE");
assert_eq!(
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index fb998510..a8c94416 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -10126,3 +10126,31 @@ fn parse_bitstring_literal_escaping() {
pg_and_generic().verified_stmt("SELECT B''''");
pg_and_generic().verified_stmt("SELECT B'it''s'");
}
+
+#[test]
+fn parse_reserved_keywords_rejected_as_identifiers() {
+ let err = pg().parse_sql_statements("SELECT to").unwrap_err();
+ assert_eq!(
+ ParserError::ParserError("Expected an expression, found: to".to_string()),
+ err
+ );
+
+ let err = pg().parse_sql_statements("SELECT on").unwrap_err();
+ assert_eq!(
+ ParserError::ParserError("Expected an expression, found: on".to_string()),
+ err
+ );
+
+ pg().parse_sql_statements("SELECT CAST").unwrap_err();
+ pg().parse_sql_statements("SELECT id, CASE FROM t")
+ .unwrap_err();
+ pg().parse_sql_statements("SELECT a = case FROM t")
+ .unwrap_err();
+ pg().parse_sql_statements("SELECT a FROM t WHERE a = select.b")
+ .unwrap_err();
+
+ pg().verified_stmt("SELECT name FROM t");
+ pg().verified_stmt("SELECT set FROM t");
+ pg().verified_expr("right('abc', 3)");
+ pg().verified_expr("left('abc', 3)");
+}
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index b8fad7e9..f5002529 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -1024,6 +1024,35 @@ fn parse_n_prefix_not_national_string() {
all_dialects_where(|d| d.supports_national_string_literal()).verified_stmt("SELECT N'hello'");
}
+#[test]
+fn parse_reserved_keywords_rejected_as_identifiers() {
+ let err = sqlite().parse_sql_statements("SELECT to").unwrap_err();
+ assert_eq!(
+ ParserError::ParserError("Expected an expression, found: to".to_string()),
+ err
+ );
+
+ let err = sqlite().parse_sql_statements("SELECT on").unwrap_err();
+ assert_eq!(
+ ParserError::ParserError("Expected an expression, found: on".to_string()),
+ err
+ );
+
+ sqlite().parse_sql_statements("SELECT CAST").unwrap_err();
+ sqlite()
+ .parse_sql_statements("SELECT id, CASE FROM t")
+ .unwrap_err();
+ sqlite()
+ .parse_sql_statements("SELECT a = case FROM t")
+ .unwrap_err();
+ sqlite()
+ .parse_sql_statements("SELECT a FROM t WHERE a = select.b")
+ .unwrap_err();
+
+ sqlite().verified_stmt("SELECT name FROM t");
+ sqlite().verified_stmt("SELECT full FROM t");
+}
+
fn sqlite() -> TestedDialects {
TestedDialects::new(vec![Box::new(SQLiteDialect {})])
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org