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