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/02 19:59:26 UTC

(datafusion-sqlparser-rs) 01/01: SQLite: Reject DML in nested queries

This is an automated email from the ASF dual-hosted git repository.

LucaCappelletti94 pushed a commit to branch upstream/sqlite-insert-value-and-nested-dml
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 86e0bf868395828942cf61315a8c4c4ff364964f
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Fri Oct 2 21:59:09 2026 +0200

    SQLite: Reject DML in nested queries
---
 src/dialect/mod.rs        |  5 +++++
 src/dialect/sqlite.rs     |  5 +++++
 src/parser/mod.rs         | 17 +++++++++++------
 tests/sqlparser_common.rs |  2 +-
 tests/sqlparser_sqlite.rs | 25 +++++++++++++++++++++++++
 5 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 2beb5655..4ad996e0 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -1457,6 +1457,11 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if the dialect accepts `INSERT`, `UPDATE`, `DELETE` or `MERGE` in a subquery, CTE or `INSERT` source.
+    fn supports_nested_dml_query(&self) -> bool {
+        true
+    }
+
     /// Returns true if this dialect supports `SET` statements without an explicit
     /// assignment operator such as `=`. For example: `SET SHOWPLAN_XML ON`.
     fn supports_set_stmt_without_operator(&self) -> bool {
diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs
index 6f9ec0d2..e679a340 100644
--- a/src/dialect/sqlite.rs
+++ b/src/dialect/sqlite.rs
@@ -142,4 +142,9 @@ impl Dialect for SQLiteDialect {
     fn supports_national_string_literal(&self) -> bool {
         false
     }
+
+    /// See <https://sqlite.org/lang_select.html>
+    fn supports_nested_dml_query(&self) -> bool {
+        false
+    }
 }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..2c5a48b7 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -650,7 +650,7 @@ impl<'a> Parser<'a> {
                 }
                 Keyword::SELECT | Keyword::WITH | Keyword::VALUES | Keyword::FROM => {
                     self.prev_token();
-                    self.parse_query().map(Into::into)
+                    self.parse_query_allowing_dml(true).map(Into::into)
                 }
                 Keyword::TRUNCATE => self.parse_truncate().map(Into::into),
                 Keyword::ATTACH => {
@@ -14728,8 +14728,13 @@ impl<'a> Parser<'a> {
     /// preceded with some `WITH` CTE declarations and optionally followed
     /// by `ORDER BY`. Unlike some other parse_... methods, this one doesn't
     /// expect the initial keyword to be already consumed
-    #[cfg_attr(feature = "recursive-protection", recursive::recursive)]
     pub fn parse_query(&mut self) -> Result<Box<Query>, ParserError> {
+        self.parse_query_allowing_dml(self.dialect.supports_nested_dml_query())
+    }
+
+    /// Parses a query whose body may be `INSERT`, `UPDATE`, `DELETE` or `MERGE` when `allow_dml` is set.
+    #[cfg_attr(feature = "recursive-protection", recursive::recursive)]
+    fn parse_query_allowing_dml(&mut self, allow_dml: bool) -> Result<Box<Query>, ParserError> {
         let _guard = self.recursion_counter.try_decrease()?;
         let with = if self.parse_keyword(Keyword::WITH) {
             let with_token = self.get_current_token();
@@ -14741,7 +14746,7 @@ impl<'a> Parser<'a> {
         } else {
             None
         };
-        if self.parse_keyword(Keyword::INSERT) {
+        if allow_dml && self.parse_keyword(Keyword::INSERT) {
             Ok(Query {
                 with,
                 body: self.parse_insert_setexpr_boxed(self.get_current_token().clone())?,
@@ -14755,7 +14760,7 @@ impl<'a> Parser<'a> {
                 pipe_operators: vec![],
             }
             .into())
-        } else if self.parse_keyword(Keyword::UPDATE) {
+        } else if allow_dml && self.parse_keyword(Keyword::UPDATE) {
             Ok(Query {
                 with,
                 body: self.parse_update_setexpr_boxed(self.get_current_token().clone())?,
@@ -14769,7 +14774,7 @@ impl<'a> Parser<'a> {
                 pipe_operators: vec![],
             }
             .into())
-        } else if self.parse_keyword(Keyword::DELETE) {
+        } else if allow_dml && self.parse_keyword(Keyword::DELETE) {
             Ok(Query {
                 with,
                 body: self.parse_delete_setexpr_boxed(self.get_current_token().clone())?,
@@ -14783,7 +14788,7 @@ impl<'a> Parser<'a> {
                 pipe_operators: vec![],
             }
             .into())
-        } else if self.parse_keyword(Keyword::MERGE) {
+        } else if allow_dml && self.parse_keyword(Keyword::MERGE) {
             Ok(Query {
                 with,
                 body: self.parse_merge_setexpr_boxed(self.get_current_token().clone())?,
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 8e08c657..73865a4d 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -10710,7 +10710,7 @@ VALUES (1, 'abc')";
 
 #[test]
 fn test_merge_in_cte() {
-    verified_only_select(
+    all_dialects_where(|d| d.supports_nested_dml_query()).verified_only_select(
         "WITH x AS (\
             MERGE INTO t USING (VALUES (1)) ON 1 = 1 \
             WHEN MATCHED THEN DELETE \
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index b8fad7e9..4753331d 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -1024,6 +1024,31 @@ fn parse_n_prefix_not_national_string() {
     all_dialects_where(|d| d.supports_national_string_literal()).verified_stmt("SELECT N'hello'");
 }
 
+#[test]
+fn parse_nested_dml_rejected() {
+    for sql in [
+        "INSERT INTO u INSERT INTO u SELECT 1",
+        "INSERT INTO u (a) INSERT INTO u SELECT 1",
+        "INSERT INTO u WITH x AS (SELECT 1) INSERT INTO u SELECT 1",
+        "SELECT * FROM (INSERT INTO u SELECT 1)",
+        "WITH x AS (INSERT INTO u SELECT 1) SELECT 1",
+        "WITH x AS (UPDATE u SET a = 1) SELECT 1",
+        "WITH x AS (DELETE FROM u) SELECT 1",
+    ] {
+        assert!(sqlite().parse_sql_statements(sql).is_err(), "{sql}");
+    }
+
+    let nested_dml = all_dialects_where(|d| d.supports_nested_dml_query());
+    nested_dml.verified_stmt("INSERT INTO u (a) INSERT INTO u SELECT 1");
+    nested_dml.verified_stmt("SELECT * FROM (INSERT INTO u SELECT 1)");
+    nested_dml.verified_stmt("WITH x AS (INSERT INTO u SELECT 1) SELECT 1");
+
+    sqlite().verified_stmt("WITH x AS (SELECT 1) INSERT INTO u SELECT * FROM x");
+    sqlite().verified_stmt("WITH x AS (SELECT 1) UPDATE u SET a = 2");
+    sqlite().verified_stmt("WITH x AS (SELECT 1) DELETE FROM u");
+    sqlite().verified_stmt("INSERT INTO u (a) WITH x AS (SELECT 1) SELECT * FROM x");
+}
+
 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