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/05 14:53:34 UTC

(datafusion-sqlparser-rs) branch upstream/sqlite-postfix-colon updated (b95f1b44 -> b21d1a5f)

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

LucaCappelletti94 pushed a change to branch upstream/sqlite-postfix-colon
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


 discard b95f1b44 SQLite: Reject colon-based semi-structured data traversal
     new b21d1a5f SQLite/Postgres: Reject invalid colon and function-argument syntax

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   (b95f1b44)
            \
             N -- N -- N   refs/heads/upstream/sqlite-postfix-colon (b21d1a5f)

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:
 derive/src/dialect.rs       |  4 +++-
 src/dialect/mod.rs          | 17 ++++++++++++++++-
 src/dialect/postgresql.rs   | 13 +++++++++----
 src/parser/mod.rs           | 46 ++++++++++++++++++++++++++++++++-------------
 tests/sqlparser_postgres.rs | 19 +++++++++++++++++++
 5 files changed, 80 insertions(+), 19 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: SQLite/Postgres: Reject invalid colon and function-argument syntax

Posted by lu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

LucaCappelletti94 pushed a commit to branch upstream/sqlite-postfix-colon
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit b21d1a5f53603b84a4f9bc6351ab3c6e752a8ce2
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Mon Oct 5 16:53:28 2026 +0200

    SQLite/Postgres: Reject invalid colon and function-argument syntax
---
 derive/src/dialect.rs       |  4 +++-
 src/dialect/mod.rs          | 17 ++++++++++++++++-
 src/dialect/postgresql.rs   | 13 +++++++++----
 src/dialect/sqlite.rs       |  9 +++++++++
 src/parser/mod.rs           | 46 ++++++++++++++++++++++++++++++++-------------
 tests/sqlparser_postgres.rs | 19 +++++++++++++++++++
 tests/sqlparser_sqlite.rs   | 19 +++++++++++++++++++
 7 files changed, 108 insertions(+), 19 deletions(-)

diff --git a/derive/src/dialect.rs b/derive/src/dialect.rs
index af17067a..aadfaef3 100644
--- a/derive/src/dialect.rs
+++ b/derive/src/dialect.rs
@@ -229,7 +229,9 @@ fn generate_derived_dialect(input: &DeriveDialectInput, methods: &[DialectMethod
         const _: () = {
             use ::core::iter::Peekable;
             use ::core::str::Chars;
-            use sqlparser::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement};
+            use sqlparser::ast::{
+                ColumnOption, Expr, GranteesType, Ident, ObjectName, ObjectNamePart, Statement,
+            };
             use sqlparser::dialect::{Dialect, Precedence};
             use sqlparser::keywords::Keyword;
             use sqlparser::parser::{Parser, ParserError};
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 2beb5655..cbb50bc2 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -132,7 +132,7 @@ pub use self::teradata::TeradataDialect;
 #[cfg(feature = "derive-dialect")]
 pub use sqlparser_derive::derive_dialect;
 
-use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement};
+use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectName, ObjectNamePart, Statement};
 pub use crate::keywords;
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
@@ -481,6 +481,14 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if `fn_name` supports named arguments of the form
+    /// `FUN(a : '1', b : '2')`. Defaults to
+    /// [`Dialect::supports_named_fn_args_with_colon_operator`], ignoring `fn_name`.
+    fn supports_named_fn_args_with_colon_operator_for(&self, fn_name: Option<&ObjectName>) -> bool {
+        let _ = fn_name;
+        self.supports_named_fn_args_with_colon_operator()
+    }
+
     /// Returns true if the dialect supports named arguments of the form `FUN(a := '1', b := '2')`.
     fn supports_named_fn_args_with_assignment_operator(&self) -> bool {
         false
@@ -1934,6 +1942,13 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if `fn_name` supports `FUN(a AS x)`-style aliased arguments.
+    /// Defaults to [`Dialect::supports_aliased_function_args`], ignoring `fn_name`.
+    fn supports_aliased_function_args_for(&self, fn_name: Option<&ObjectName>) -> bool {
+        let _ = fn_name;
+        self.supports_aliased_function_args()
+    }
+
     /// Returns true if the dialect supports `USING <format>` in `CREATE TABLE`.
     ///
     /// Example:
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs
index 3bec6ceb..bc49f4b3 100644
--- a/src/dialect/postgresql.rs
+++ b/src/dialect/postgresql.rs
@@ -28,6 +28,7 @@
 // limitations under the License.
 use log::debug;
 
+use crate::ast::ObjectName;
 use crate::dialect::{Dialect, Precedence};
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
@@ -261,8 +262,8 @@ impl Dialect for PostgreSqlDialect {
     /// ```sql
     /// SELECT json_object('a': 'b')
     /// ```
-    fn supports_named_fn_args_with_colon_operator(&self) -> bool {
-        true
+    fn supports_named_fn_args_with_colon_operator_for(&self, fn_name: Option<&ObjectName>) -> bool {
+        fn_name.is_some_and(|name| Parser::is_simple_unquoted_object_name(name, "json_object"))
     }
 
     /// See <https://www.postgresql.org/docs/current/functions-json.html>
@@ -357,8 +358,12 @@ impl Dialect for PostgreSqlDialect {
         true
     }
 
-    fn supports_aliased_function_args(&self) -> bool {
-        true
+    fn supports_aliased_function_args_for(&self, fn_name: Option<&ObjectName>) -> bool {
+        fn_name.is_some_and(|name| {
+            ["XMLFOREST", "XMLELEMENT", "XMLTABLE"]
+                .iter()
+                .any(|f| Parser::is_simple_unquoted_object_name(name, f))
+        })
     }
 
     /// Postgres supports query optimizer hints via the `pg_hint_plan` extension,
diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs
index 6f9ec0d2..a01ced32 100644
--- a/src/dialect/sqlite.rs
+++ b/src/dialect/sqlite.rs
@@ -23,6 +23,7 @@ use crate::ast::{Expr, Statement};
 use crate::dialect::Dialect;
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
+use crate::tokenizer::Token;
 
 /// A [`Dialect`] for [SQLite](https://www.sqlite.org)
 ///
@@ -97,6 +98,14 @@ impl Dialect for SQLiteDialect {
         None
     }
 
+    fn get_next_precedence(&self, parser: &Parser) -> Option<Result<u8, ParserError>> {
+        match &parser.peek_token_ref().token {
+            // lowest prec to prevent it from turning into a binary op
+            Token::Colon => Some(Ok(self.prec_unknown())),
+            _ => None,
+        }
+    }
+
     fn supports_in_empty_list(&self) -> bool {
         true
     }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..fb4f51a6 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1745,7 +1745,7 @@ impl<'a> Parser<'a> {
 
     /// Returns true if the given [ObjectName] is a single unquoted
     /// identifier matching `expected` (case-insensitive).
-    fn is_simple_unquoted_object_name(name: &ObjectName, expected: &str) -> bool {
+    pub(crate) fn is_simple_unquoted_object_name(name: &ObjectName, expected: &str) -> bool {
         if let [ObjectNamePart::Identifier(ident)] = name.0.as_slice() {
             ident.quote_style.is_none() && ident.value.eq_ignore_ascii_case(expected)
         } else {
@@ -2585,7 +2585,7 @@ impl<'a> Parser<'a> {
         {
             self.parse_xmlparse_argument_list()?
         } else {
-            self.parse_function_argument_list()?
+            self.parse_function_argument_list(&name)?
         };
         let mut parameters = FunctionArguments::None;
         // ClickHouse aggregations support parametric functions like `HISTOGRAM(0.5, 0.6)(x, y)`
@@ -2594,7 +2594,7 @@ impl<'a> Parser<'a> {
             && self.consume_token(&Token::LParen)
         {
             parameters = FunctionArguments::List(args);
-            args = self.parse_function_argument_list()?;
+            args = self.parse_function_argument_list(&name)?;
         }
 
         let within_group = if self.parse_keywords(&[Keyword::WITHIN, Keyword::GROUP]) {
@@ -2673,7 +2673,7 @@ impl<'a> Parser<'a> {
     /// Parse time-related function `name` possibly followed by `(...)` arguments.
     pub fn parse_time_functions(&mut self, name: ObjectName) -> Result<Expr, ParserError> {
         let args = if self.consume_token(&Token::LParen) {
-            FunctionArguments::List(self.parse_function_argument_list()?)
+            FunctionArguments::List(self.parse_function_argument_list(&name)?)
         } else {
             FunctionArguments::None
         };
@@ -19027,6 +19027,15 @@ impl<'a> Parser<'a> {
 
     /// Parse a single function argument, handling named and unnamed variants.
     pub fn parse_function_args(&mut self) -> Result<FunctionArg, ParserError> {
+        self.parse_function_args_for(None)
+    }
+
+    /// Like [`Parser::parse_function_args`], but `fn_name`, when known, scopes
+    /// dialect-specific named-argument syntax to the functions that define it.
+    fn parse_function_args_for(
+        &mut self,
+        fn_name: Option<&ObjectName>,
+    ) -> Result<FunctionArg, ParserError> {
         // Parse the argument expression once, then check for a named-arg
         // operator. Parsing it speculatively and re-parsing on the unnamed
         // path is O(2^depth) on nested calls like `CAST(CASE (CAST(CASE (…`.
@@ -19035,7 +19044,7 @@ impl<'a> Parser<'a> {
             // A wildcard is never a named-arg name; only the unnamed form applies.
             if !matches!(expr, Expr::Wildcard(_) | Expr::QualifiedWildcard(..)) {
                 if let Some(operator) =
-                    self.maybe_parse(|p| p.parse_function_named_arg_operator())?
+                    self.maybe_parse(|p| p.parse_function_named_arg_operator(fn_name))?
                 {
                     let arg = self.parse_wildcard_expr()?.into();
                     return Ok(FunctionArg::ExprNamed {
@@ -19047,13 +19056,13 @@ impl<'a> Parser<'a> {
             }
             let arg_expr = self.function_arg_expr_from_wildcard(expr)?;
             return Ok(FunctionArg::Unnamed(
-                self.maybe_parse_aliased_function_arg(arg_expr)?,
+                self.maybe_parse_aliased_function_arg(arg_expr, fn_name)?,
             ));
         }
 
         let arg = self.maybe_parse(|p| {
             let name = p.parse_identifier()?;
-            let operator = p.parse_function_named_arg_operator()?;
+            let operator = p.parse_function_named_arg_operator(fn_name)?;
             let arg = p.parse_wildcard_expr()?.into();
             Ok(FunctionArg::Named {
                 name,
@@ -19067,7 +19076,7 @@ impl<'a> Parser<'a> {
         let wildcard_expr = self.parse_wildcard_expr()?;
         let arg_expr = self.function_arg_expr_from_wildcard(wildcard_expr)?;
         Ok(FunctionArg::Unnamed(
-            self.maybe_parse_aliased_function_arg(arg_expr)?,
+            self.maybe_parse_aliased_function_arg(arg_expr, fn_name)?,
         ))
     }
 
@@ -19101,10 +19110,11 @@ impl<'a> Parser<'a> {
     fn maybe_parse_aliased_function_arg(
         &mut self,
         arg_expr: FunctionArgExpr,
+        fn_name: Option<&ObjectName>,
     ) -> Result<FunctionArgExpr, ParserError> {
         Ok(match arg_expr {
             FunctionArgExpr::Expr(expr)
-                if self.dialect.supports_aliased_function_args()
+                if self.dialect.supports_aliased_function_args_for(fn_name)
                     && self.parse_keyword(Keyword::AS) =>
             {
                 FunctionArgExpr::Expr(Expr::Named {
@@ -19116,7 +19126,10 @@ impl<'a> Parser<'a> {
         })
     }
 
-    fn parse_function_named_arg_operator(&mut self) -> Result<FunctionArgOperator, ParserError> {
+    fn parse_function_named_arg_operator(
+        &mut self,
+        fn_name: Option<&ObjectName>,
+    ) -> Result<FunctionArgOperator, ParserError> {
         if self.parse_keyword(Keyword::VALUE) {
             return Ok(FunctionArgOperator::Value);
         }
@@ -19135,7 +19148,11 @@ impl<'a> Parser<'a> {
             {
                 Ok(FunctionArgOperator::Assignment)
             }
-            Token::Colon if self.dialect.supports_named_fn_args_with_colon_operator() => {
+            Token::Colon
+                if self
+                    .dialect
+                    .supports_named_fn_args_with_colon_operator_for(fn_name) =>
+            {
                 Ok(FunctionArgOperator::Colon)
             }
             _ => {
@@ -19185,7 +19202,10 @@ impl<'a> Parser<'a> {
     /// FIRST_VALUE(x ORDER BY 1,2,3);
     /// FIRST_VALUE(x IGNORE NULL);
     /// ```
-    fn parse_function_argument_list(&mut self) -> Result<FunctionArgumentList, ParserError> {
+    fn parse_function_argument_list(
+        &mut self,
+        fn_name: &ObjectName,
+    ) -> Result<FunctionArgumentList, ParserError> {
         let mut clauses = vec![];
 
         // Handle clauses that may exist with an empty argument list
@@ -19209,7 +19229,7 @@ impl<'a> Parser<'a> {
         }
 
         let duplicate_treatment = self.parse_duplicate_treatment()?;
-        let args = self.parse_comma_separated(Parser::parse_function_args)?;
+        let args = self.parse_comma_separated(|p| p.parse_function_args_for(Some(fn_name)))?;
 
         if self.parse_keyword(Keyword::WHERE) {
             clauses.push(FunctionArgumentClause::Where(self.parse_expr()?));
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index fb998510..09809e32 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -4144,6 +4144,25 @@ fn parse_xmlforest_aliased_arguments() {
     );
 }
 
+#[test]
+fn parse_named_fn_args_scoped_to_their_functions() {
+    assert_eq!(
+        pg().parse_sql_statements("SELECT abs(a:b) FROM t")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Expected: ), found: :"
+    );
+    assert_eq!(
+        pg().parse_sql_statements("SELECT abs(a AS x) FROM t")
+            .unwrap_err()
+            .to_string(),
+        "sql parser error: Expected: ), found: AS"
+    );
+
+    pg().verified_expr("JSON_OBJECT('a' : 'b')");
+    pg_and_generic().verified_only_select("SELECT XMLELEMENT(a AS x, b)");
+}
+
 #[test]
 fn parse_xmlparse() {
     // The parser only distinguishes the two modes, so the corpus covers those
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index b8fad7e9..9c332c36 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -1024,6 +1024,25 @@ fn parse_n_prefix_not_national_string() {
     all_dialects_where(|d| d.supports_national_string_literal()).verified_stmt("SELECT N'hello'");
 }
 
+#[test]
+fn parse_colon_does_not_start_json_access() {
+    // SQLite rejects `SELECT c:se` (`near ":se": syntax error`); no `:` traversal.
+    assert!(sqlite().parse_sql_statements("SELECT c:se").is_err());
+    assert!(sqlite().parse_sql_statements("SELECT a:b FROM t").is_err());
+
+    let ast = sqlite().verified_only_select("SELECT :xxx");
+    assert_eq!(
+        ast.projection[0],
+        UnnamedExpr(Expr::Value(
+            (Value::Placeholder(":xxx".into())).with_empty_span()
+        )),
+    );
+
+    assert!(TestedDialects::new(vec![Box::new(GenericDialect {})])
+        .parse_sql_statements("SELECT a:b FROM t")
+        .is_ok());
+}
+
 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