You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/09/26 12:06:18 UTC

(datafusion-sqlparser-rs) branch main updated: ClickHouse: Support tuple element access by index (#2574)

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

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 91f7ce279 ClickHouse: Support tuple element access by index (#2574)
91f7ce279 is described below

commit 91f7ce27923c0b39fa49525c5f03057247c9732a
Author: Dmitri B. <s5...@proton.me>
AuthorDate: Sat Sep 26 12:04:33 2026 +0000

    ClickHouse: Support tuple element access by index (#2574)
---
 .../fuzz_seeds/clickhouse_tuple_element_access.sql |  1 +
 src/dialect/clickhouse.rs                          |  5 ++
 src/dialect/mod.rs                                 |  6 ++
 src/parser/mod.rs                                  |  2 +
 src/tokenizer.rs                                   | 96 ++++++++++++++++++++++
 tests/sqlparser_clickhouse.rs                      | 62 ++++++++++++++
 6 files changed, 172 insertions(+)

diff --git a/fuzz/fuzz_seeds/clickhouse_tuple_element_access.sql b/fuzz/fuzz_seeds/clickhouse_tuple_element_access.sql
new file mode 100644
index 000000000..ee21b503d
--- /dev/null
+++ b/fuzz/fuzz_seeds/clickhouse_tuple_element_access.sql
@@ -0,0 +1 @@
+SELECT t.1, t.2.1, (1, 'a').2, tuple(1, (2, 3)).2.1, [(1, 2)][1].1 FROM (SELECT (1, (2, 3)) AS t) WHERE t.2.2 = 3
\ No newline at end of file
diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs
index 908075827..d5c130d04 100644
--- a/src/dialect/clickhouse.rs
+++ b/src/dialect/clickhouse.rs
@@ -178,4 +178,9 @@ impl Dialect for ClickHouseDialect {
     fn supports_alter_column_position(&self) -> bool {
         true
     }
+
+    /// See <https://clickhouse.com/docs/sql-reference/data-types/tuple#addressing-tuple-elements>
+    fn supports_tuple_element_access(&self) -> bool {
+        true
+    }
 }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index e3e79281a..4bd80d580 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -505,6 +505,12 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if a period directly after an identifier starts a tuple
+    /// element access such as `t.1`, instead of the number `.1`.
+    fn supports_tuple_element_access(&self) -> bool {
+        false
+    }
+
     /// Returns true if the dialect supports numbers containing underscores, e.g. `10_000_000`
     fn supports_numeric_literal_underscores(&self) -> bool {
         false
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 9ae1de545..f7fc6d33b 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1360,6 +1360,8 @@ impl<'a> Parser<'a> {
                                 AttachedToken(next_token),
                             ));
                         }
+                        // A field index such as `t.1` is parsed as an expression.
+                        Token::Number(..) if self.dialect.supports_tuple_element_access() => break,
                         _ => {
                             return self.expected("an identifier or a '*' after '.'", next_token);
                         }
diff --git a/src/tokenizer.rs b/src/tokenizer.rs
index 7d363793c..423bbea31 100644
--- a/src/tokenizer.rs
+++ b/src/tokenizer.rs
@@ -1354,8 +1354,34 @@ impl<'a> Tokenizer<'a> {
                         );
                     }
 
+                    // A period directly after an identifier, `)`, `]` or a tuple index
+                    // starts a field access such as `t.1`, `(1, 2).1` or `t.1.2`, never a float.
+                    if ch == '.'
+                        && self.dialect.supports_tuple_element_access()
+                        && matches!(
+                            prev_token,
+                            Some(
+                                Token::Word(_)
+                                    | Token::RParen
+                                    | Token::RBracket
+                                    | Token::Number(..)
+                            )
+                        )
+                    {
+                        chars.next();
+                        return Ok(Some(Token::Period));
+                    }
+
                     let mut s = self.tokenize_number_part(chars, |ch| ch.is_ascii_digit())?;
 
+                    // A tuple index is an integer, so `t.1.2` is `(t.1).2`.
+                    if !s.is_empty()
+                        && self.dialect.supports_tuple_element_access()
+                        && prev_token == Some(&Token::Period)
+                    {
+                        return Ok(Some(Token::Number(s, false)));
+                    }
+
                     // match binary literal that starts with 0x
                     if s == "0" && chars.peek() == Some(&'x') {
                         chars.next();
@@ -4580,4 +4606,74 @@ mod tests {
             "[a b]"
         );
     }
+
+    #[test]
+    fn tokenize_period_before_digits_after_identifier_or_bracket() {
+        let dialect = ClickHouseDialect {};
+        for (sql, expected) in [
+            (
+                "t.1",
+                vec![
+                    Token::make_word("t", None),
+                    Token::Period,
+                    Token::Number("1".to_string(), false),
+                ],
+            ),
+            (
+                "(1).1",
+                vec![
+                    Token::LParen,
+                    Token::Number("1".to_string(), false),
+                    Token::RParen,
+                    Token::Period,
+                    Token::Number("1".to_string(), false),
+                ],
+            ),
+            (
+                "a[1].1",
+                vec![
+                    Token::make_word("a", None),
+                    Token::LBracket,
+                    Token::Number("1".to_string(), false),
+                    Token::RBracket,
+                    Token::Period,
+                    Token::Number("1".to_string(), false),
+                ],
+            ),
+            (
+                "t.1.2",
+                vec![
+                    Token::make_word("t", None),
+                    Token::Period,
+                    Token::Number("1".to_string(), false),
+                    Token::Period,
+                    Token::Number("2".to_string(), false),
+                ],
+            ),
+            (
+                "t .1",
+                vec![
+                    Token::make_word("t", None),
+                    Token::Whitespace(Whitespace::Space),
+                    Token::Number(".1".to_string(), false),
+                ],
+            ),
+            ("1.5", vec![Token::Number("1.5".to_string(), false)]),
+            (".5", vec![Token::Number(".5".to_string(), false)]),
+        ] {
+            let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap();
+            compare(expected, tokens);
+        }
+
+        let tokens = Tokenizer::new(&GenericDialect {}, "t.1")
+            .tokenize()
+            .unwrap();
+        compare(
+            vec![
+                Token::make_word("t", None),
+                Token::Number(".1".to_string(), false),
+            ],
+            tokens,
+        );
+    }
 }
diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs
index affba45aa..17d558526 100644
--- a/tests/sqlparser_clickhouse.rs
+++ b/tests/sqlparser_clickhouse.rs
@@ -2022,6 +2022,68 @@ fn parse_object_type_parameter() {
     }
 }
 
+#[test]
+fn parse_tuple_element_access() {
+    for (sql, canonical) in [
+        ("SELECT t.1 FROM t", "SELECT t . 1 FROM t"),
+        (
+            "SELECT t.1 AS a, t.2 AS b FROM (SELECT (1, 'x') AS t)",
+            "SELECT t . 1 AS a, t . 2 AS b FROM (SELECT (1, 'x') AS t)",
+        ),
+        ("SELECT (1, 'a').1", "SELECT (1, 'a') . 1"),
+        ("SELECT tuple(1, 'a').2", "SELECT tuple(1, 'a') . 2"),
+        ("SELECT arr[1].1 FROM t", "SELECT arr[1] . 1 FROM t"),
+        ("SELECT `t`.1 FROM t", "SELECT `t` . 1 FROM t"),
+        ("SELECT t.1 + 1 FROM t", "SELECT t . 1 + 1 FROM t"),
+        (
+            "SELECT * FROM t WHERE t.1 = 1",
+            "SELECT * FROM t WHERE t . 1 = 1",
+        ),
+        ("SELECT t.1.2 FROM t", "SELECT t . 1 . 2 FROM t"),
+        (
+            "SELECT ((1, 2), 3).1.2, tuple(1, (2, 3)).2.1",
+            "SELECT ((1, 2), 3) . 1 . 2, tuple(1, (2, 3)) . 2 . 1",
+        ),
+    ] {
+        clickhouse().one_statement_parses_to(sql, canonical);
+    }
+
+    let select = clickhouse().verified_only_select("SELECT t . 1 FROM t");
+    assert_eq!(
+        select.projection[0],
+        UnnamedExpr(Expr::CompoundFieldAccess {
+            root: Box::new(Identifier(Ident::new("t"))),
+            access_chain: vec![AccessExpr::Dot(Expr::Value(number("1").with_empty_span()))],
+        })
+    );
+
+    let select = clickhouse().verified_only_select("SELECT t . 1 . 2 FROM t");
+    assert_eq!(
+        select.projection[0],
+        UnnamedExpr(Expr::CompoundFieldAccess {
+            root: Box::new(Identifier(Ident::new("t"))),
+            access_chain: vec![
+                AccessExpr::Dot(Expr::Value(number("1").with_empty_span())),
+                AccessExpr::Dot(Expr::Value(number("2").with_empty_span())),
+            ],
+        })
+    );
+
+    clickhouse().verified_stmt("SELECT 1.5, 1 + 0.5");
+    clickhouse().one_statement_parses_to("SELECT .5, 1e3", "SELECT 0.5, 1000");
+
+    assert!(clickhouse().parse_sql_statements("SELECT t.").is_err());
+    assert!(clickhouse().parse_sql_statements("SELECT t.1.").is_err());
+
+    let unsupported = all_dialects_where(|d| !d.supports_tuple_element_access());
+    unsupported.verified_stmt("SELECT (1, 'a') . 1");
+    for dialect in unsupported.dialects {
+        assert!(TestedDialects::new(vec![dialect])
+            .parse_sql_statements("SELECT t.1 FROM t")
+            .is_err());
+    }
+}
+
 fn clickhouse() -> TestedDialects {
     TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
 }


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