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/19 18:15:23 UTC
(datafusion-sqlparser-rs) branch sqlite-in-table updated: SQLite: Reject reserved keywords as the table after IN
This is an automated email from the ASF dual-hosted git repository.
LucaCappelletti94 pushed a commit to branch sqlite-in-table
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
The following commit(s) were added to refs/heads/sqlite-in-table by this push:
new 82c6b85ad SQLite: Reject reserved keywords as the table after IN
82c6b85ad is described below
commit 82c6b85ad713e2db94efeb10d2a4f197e0d364a2
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Mon Oct 19 20:15:17 2026 +0200
SQLite: Reject reserved keywords as the table after IN
---
src/dialect/sqlite.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++
src/parser/mod.rs | 16 ++++++++++-
tests/sqlparser_sqlite.rs | 29 ++++++++++++++++++++
3 files changed, 111 insertions(+), 1 deletion(-)
diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs
index 6a6e5b4e7..432f52ef0 100644
--- a/src/dialect/sqlite.rs
+++ b/src/dialect/sqlite.rs
@@ -150,4 +150,71 @@ impl Dialect for SQLiteDialect {
fn supports_in_table_expression(&self) -> bool {
true
}
+
+ /// The keywords SQLite does not accept as a name, as opposed to the
+ /// keywords its grammar lets fall back to an identifier.
+ /// See <https://www.sqlite.org/lang_keywords.html> and the `%fallback ID`
+ /// list in [parse.y](https://sqlite.org/src/file?name=src/parse.y)
+ fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
+ !matches!(
+ kw,
+ Keyword::ADD
+ | Keyword::ALL
+ | Keyword::ALTER
+ | Keyword::AND
+ | Keyword::AS
+ | Keyword::AUTOINCREMENT
+ | Keyword::BETWEEN
+ | Keyword::CASE
+ | 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::NULL
+ | Keyword::ON
+ | Keyword::OR
+ | Keyword::ORDER
+ | Keyword::PRIMARY
+ | 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
+ )
+ }
}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 8878a944a..5f5447831 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -4483,7 +4483,7 @@ impl<'a> Parser<'a> {
InTarget::Unnest(Box::new(array_expr))
} else if !global
&& self.dialect.supports_in_table_expression()
- && matches!(self.peek_token_ref().token, Token::Word(_))
+ && self.next_word_is_table_name()
{
let name = self.parse_object_name(false)?;
let args = if self.consume_token(&Token::LParen) {
@@ -4511,6 +4511,20 @@ impl<'a> Parser<'a> {
})
}
+ /// Returns true if the next token is a word the dialect accepts as a table
+ /// name. The word is consumed while asking, so `is_table_factor` peeks past
+ /// it as it does at the trailing comma check.
+ fn next_word_is_table_name(&mut self) -> bool {
+ let Token::Word(word) = &self.peek_token_ref().token else {
+ return false;
+ };
+ let keyword = word.keyword;
+ self.advance_token();
+ let is_name = self.dialect.is_table_factor(&keyword, self);
+ self.prev_token();
+ is_name
+ }
+
/// Parses the parenthesized subquery or value list of an `IN` predicate.
fn parse_in_parenthesized_target(&mut self) -> Result<InTarget, ParserError> {
self.expect_token(&Token::LParen)?;
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index 3e3104947..f16c527b2 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -1175,3 +1175,32 @@ fn test_dollar_sign_in_identifier() {
.parse_sql_statements("SELECT a$ FROM t")
.unwrap_err();
}
+
+#[test]
+fn parse_in_table_keyword_names() {
+ // Keywords SQLite lets fall back to an identifier name a table after `IN`.
+ for sql in [
+ "SELECT 1 IN view",
+ "SELECT 1 IN offset",
+ "SELECT 1 IN key",
+ "SELECT 1 IN abort",
+ ] {
+ sqlite().verified_stmt(sql);
+ }
+ // Its reserved keywords do not, so the target falls back to a parenthesized list.
+ for (sql, found) in [
+ ("SELECT 1 IN FROM t", "FROM"),
+ ("SELECT 1 IN select", "select"),
+ ("SELECT 1 IN where", "where"),
+ ("SELECT 1 IN NULL", "NULL"),
+ ("SELECT 1 IN table", "table"),
+ ("SELECT a FROM t WHERE a IN AND b", "AND"),
+ ] {
+ assert_eq!(
+ sqlite().parse_sql_statements(sql).unwrap_err(),
+ ParserError::ParserError(format!("Expected: (, found: {found}")),
+ );
+ }
+ // A quoted keyword is a plain name.
+ sqlite().verified_stmt(r#"SELECT 1 IN "from""#);
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org