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:33:03 UTC

(datafusion-sqlparser-rs) branch upstream/colon-access-dialect-gate updated (9bea0e2c -> 7569dc71)

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

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


 discard 9bea0e2c Gate infix colon JSON access and literal/placeholder field access by dialect
     add 14cbf755 SQLite: Support identifier characters above U+FFFF (#2555)
     new 7569dc71 Gate infix colon JSON access and literal/placeholder field access by dialect

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   (9bea0e2c)
            \
             N -- N -- N   refs/heads/upstream/colon-access-dialect-gate (7569dc71)

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: Gate infix colon JSON access and literal/placeholder field access by dialect

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

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

commit 7569dc71743c5fe311f80369a6c158cd7e6a95c9
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Fri Oct 2 20:52:52 2026 +0200

    Gate infix colon JSON access and literal/placeholder field access by dialect
---
 derive/src/dialect.rs       |  2 +-
 src/dialect/databricks.rs   |  4 ++++
 src/dialect/generic.rs      |  4 ++++
 src/dialect/mod.rs          | 28 ++++++++++++++++++++--------
 src/dialect/postgresql.rs   |  6 ++++++
 src/dialect/snowflake.rs    |  4 ++++
 src/dialect/sqlite.rs       |  7 ++++++-
 src/parser/mod.rs           | 11 ++++++++---
 tests/sqlparser_common.rs   | 27 +++++++++++++++++++++------
 tests/sqlparser_postgres.rs | 27 +++++++++++++++++++++++++++
 tests/sqlparser_sqlite.rs   | 12 +++++++++++-
 11 files changed, 112 insertions(+), 20 deletions(-)

diff --git a/derive/src/dialect.rs b/derive/src/dialect.rs
index af17067a..0aad90ae 100644
--- a/derive/src/dialect.rs
+++ b/derive/src/dialect.rs
@@ -229,7 +229,7 @@ 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, ObjectNamePart, Statement, Value};
             use sqlparser::dialect::{Dialect, Precedence};
             use sqlparser::keywords::Keyword;
             use sqlparser::parser::{Parser, ParserError};
diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs
index ca8610dd..9efd03e8 100644
--- a/src/dialect/databricks.rs
+++ b/src/dialect/databricks.rs
@@ -48,6 +48,10 @@ impl Dialect for DatabricksDialect {
         true
     }
 
+    fn supports_semi_structured_data_traversal(&self) -> bool {
+        true
+    }
+
     fn supports_filter_during_aggregation(&self) -> bool {
         true
     }
diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index 09a55185..b83b15b3 100644
--- a/src/dialect/generic.rs
+++ b/src/dialect/generic.rs
@@ -28,6 +28,10 @@ impl Dialect for GenericDialect {
         true
     }
 
+    fn supports_semi_structured_data_traversal(&self) -> bool {
+        true
+    }
+
     fn is_delimited_identifier_start(&self, ch: char) -> bool {
         ch == '"' || ch == '`'
     }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 2beb5655..0285139f 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, ObjectNamePart, Statement, Value};
 pub use crate::keywords;
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
@@ -973,13 +973,15 @@ pub trait Dialect: Debug + Any {
             Token::DoubleColon | Token::ExclamationMark | Token::LBracket | Token::CaretAt => {
                 Ok(p!(DoubleColon))
             }
-            Token::Colon => match &parser.peek_nth_token_ref(1).token {
-                // When colon is followed by a string or a number, it's usually in MAP syntax.
-                Token::SingleQuotedString(_) | Token::Number(_, _) => Ok(self.prec_unknown()),
-                // In other cases, it's used in semi-structured data traversal like in variant or JSON
-                // string columns. See `JsonAccess`.
-                _ => Ok(p!(Colon)),
-            },
+            Token::Colon if self.supports_semi_structured_data_traversal() => {
+                match &parser.peek_nth_token_ref(1).token {
+                    // When colon is followed by a string or a number, it's usually in MAP syntax.
+                    Token::SingleQuotedString(_) | Token::Number(_, _) => Ok(self.prec_unknown()),
+                    // In other cases, it's used in semi-structured data traversal like in variant or JSON
+                    // string columns. See `JsonAccess`.
+                    _ => Ok(p!(Colon)),
+                }
+            }
             Token::Arrow
             | Token::LongArrow
             | Token::HashArrow
@@ -1361,6 +1363,16 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if the dialect supports traversing semi-structured data with the infix `:` operator, e.g. `a:b`.
+    fn supports_semi_structured_data_traversal(&self) -> bool {
+        false
+    }
+
+    /// Returns true if `value` may root a `.field` or `[index]` access chain under this dialect.
+    fn supports_value_access(&self, _value: &Value) -> bool {
+        true
+    }
+
     /// Returns true if the dialect supports object-unpivot table factors in the FROM clause.
     ///
     /// Syntax:
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs
index 3bec6ceb..c1a81be3 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::Value;
 use crate::dialect::{Dialect, Precedence};
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
@@ -368,4 +369,9 @@ impl Dialect for PostgreSqlDialect {
     fn supports_comment_optimizer_hint(&self) -> bool {
         true
     }
+
+    /// `c_expr: PARAM opt_indirection` allows only a numbered parameter (`$1`).
+    fn supports_value_access(&self, value: &Value) -> bool {
+        matches!(value, Value::Placeholder(name) if name.starts_with('$'))
+    }
 }
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 3ebc1686..cb02f2c6 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -507,6 +507,10 @@ impl Dialect for SnowflakeDialect {
         true
     }
 
+    fn supports_semi_structured_data_traversal(&self) -> bool {
+        true
+    }
+
     fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool {
         match kw {
             // The following keywords can be considered an alias as long as
diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs
index 6f9ec0d2..eb434edb 100644
--- a/src/dialect/sqlite.rs
+++ b/src/dialect/sqlite.rs
@@ -19,7 +19,7 @@
 use alloc::boxed::Box;
 
 use crate::ast::BinaryOperator;
-use crate::ast::{Expr, Statement};
+use crate::ast::{Expr, Statement, Value};
 use crate::dialect::Dialect;
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
@@ -142,4 +142,9 @@ impl Dialect for SQLiteDialect {
     fn supports_national_string_literal(&self) -> bool {
         false
     }
+
+    /// SQLite's `nm ::= id | STRING` grammar takes a quoted string as a table qualifier.
+    fn supports_value_access(&self, value: &Value) -> bool {
+        matches!(value, Value::SingleQuotedString(_))
+    }
 }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..8fa5139c 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -2082,8 +2082,12 @@ impl<'a> Parser<'a> {
         mut chain: Vec<AccessExpr>,
     ) -> Result<Expr, ParserError> {
         let mut ending_wildcard: Option<TokenWithSpan> = None;
+        let root_value_access_allowed = match &root {
+            Expr::Value(v) => self.dialect.supports_value_access(&v.value),
+            _ => true,
+        };
         loop {
-            if self.consume_token(&Token::Period) {
+            if root_value_access_allowed && self.consume_token(&Token::Period) {
                 let next_token = self.peek_token_ref();
                 match &next_token.token {
                     Token::Mul => {
@@ -2183,7 +2187,8 @@ impl<'a> Parser<'a> {
                         }
                     }
                 }
-            } else if !self.dialect.supports_partiql()
+            } else if root_value_access_allowed
+                && !self.dialect.supports_partiql()
                 && self.peek_token_ref().token == Token::LBracket
             {
                 self.parse_multi_dim_subscript(&mut chain)?;
@@ -4262,7 +4267,7 @@ impl<'a> Parser<'a> {
                 expr: Box::new(expr),
             })
         } else if Token::LBracket == *tok && self.dialect.supports_partiql()
-            || (Token::Colon == *tok)
+            || (Token::Colon == *tok && self.dialect.supports_semi_structured_data_traversal())
         {
             self.prev_token();
             self.parse_json_access(expr)
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 8e08c657..5c8774a8 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -20242,12 +20242,13 @@ fn parse_alter_table_column_position() {
 
 #[test]
 fn parse_compound_field_access_numeric_display() {
-    all_dialects().verified_stmt("SELECT 1 . i");
-    all_dialects().verified_stmt("SELECT (1 . i)");
-    all_dialects().verified_stmt("SELECT 1 . 2");
-    all_dialects().verified_stmt("SELECT (a . 1 . b)");
-    all_dialects().verified_stmt("SELECT 1 . i.j");
-    all_dialects().verified_stmt("SELECT 1 . 2 . 3");
+    let dialects = all_dialects_except(|d| d.is::<PostgreSqlDialect>() || d.is::<SQLiteDialect>());
+    dialects.verified_stmt("SELECT 1 . i");
+    dialects.verified_stmt("SELECT (1 . i)");
+    dialects.verified_stmt("SELECT 1 . 2");
+    dialects.verified_stmt("SELECT (a . 1 . b)");
+    dialects.verified_stmt("SELECT 1 . i.j");
+    dialects.verified_stmt("SELECT 1 . 2 . 3");
     all_dialects().verified_stmt("SELECT a.b.c");
 
     let err = all_dialects_where(|d| !d.supports_numeric_prefix())
@@ -20433,3 +20434,17 @@ fn parse_bang_not_renders_apart_from_operand() {
     dialects.verified_stmt("SET eaac_cion = ! !o");
     dialects.one_statement_parses_to("SET eaac_cion = ! ! o", "SET eaac_cion = ! !o");
 }
+
+#[test]
+fn parse_colon_json_access_requires_dialect_support() {
+    let unsupported = all_dialects_where(|d| !d.supports_semi_structured_data_traversal());
+    assert_eq!(
+        unsupported
+            .parse_sql_statements("SELECT a:b FROM t")
+            .unwrap_err(),
+        ParserError::ParserError("Expected: end of statement, found: :".to_string())
+    );
+
+    unsupported.verified_stmt("SELECT a::INT FROM t");
+    unsupported.verified_stmt("SELECT :name");
+}
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index fb998510..43a2d59e 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -10126,3 +10126,30 @@ fn parse_bitstring_literal_escaping() {
     pg_and_generic().verified_stmt("SELECT B''''");
     pg_and_generic().verified_stmt("SELECT B'it''s'");
 }
+
+#[test]
+fn parse_param_indirection() {
+    pg().verified_stmt("SELECT $1.a");
+    pg().verified_stmt("SELECT $1[1]");
+    pg().verified_stmt("SELECT $1[1].a");
+}
+
+#[test]
+fn parse_indirection_rejects_non_param_value_roots() {
+    for sql in [
+        "SELECT 'abc'.foo",
+        "SELECT c = :NA.o FROM t",
+        "SELECT 2 .id FROM t",
+    ] {
+        assert_eq!(
+            pg().parse_sql_statements(sql).unwrap_err(),
+            ParserError::ParserError("Expected: end of statement, found: .".to_string()),
+        );
+    }
+    for sql in ["SELECT 'abc'[1]", "SELECT c = :NA[0] FROM t"] {
+        assert_eq!(
+            pg().parse_sql_statements(sql).unwrap_err(),
+            ParserError::ParserError("No infix parser for token LBracket".to_string()),
+        );
+    }
+}
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index b8fad7e9..73f0fda8 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -398,7 +398,17 @@ fn parse_create_table_with_strict() {
 #[test]
 fn parse_single_quoted_identified() {
     sqlite().verified_only_select("SELECT 't'.*, t.'x' FROM 't'");
-    // TODO: add support for select 't'.x
+    sqlite().verified_only_select("SELECT 't'.x FROM 't'");
+}
+
+#[test]
+fn parse_field_access_rejects_non_string_value_roots() {
+    for sql in ["SELECT c = :NA.o FROM t", "SELECT 2 .id FROM t"] {
+        assert_eq!(
+            sqlite().parse_sql_statements(sql).unwrap_err(),
+            ParserError::ParserError("Expected: end of statement, found: .".to_string())
+        );
+    }
 }
 
 #[test]


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org