LucaCappelletti94 commented on code in PR #2574:
URL: 
https://github.com/apache/datafusion-sqlparser-rs/pull/2574#discussion_r4110909643


##########
src/tokenizer.rs:
##########
@@ -1354,6 +1354,19 @@ impl<'a> Tokenizer<'a> {
                         );
                     }
 
+                    // A period directly after an identifier, `)` or `]` 
starts a
+                    // field access such as `t.1` or `(1, 2).1`, never a float.
+                    if ch == '.'
+                        && self.dialect.supports_tuple_element_access()
+                        && matches!(
+                            prev_token,
+                            Some(Token::Word(_) | Token::RParen | 
Token::RBracket)
+                        )
+                    {
+                        chars.next();
+                        return Ok(Some(Token::Period));
+                    }
+
                     let mut s = self.tokenize_number_part(chars, |ch| 
ch.is_ascii_digit())?;

Review Comment:
   You should lex only the integer after a period and split the period after a 
tuple index, as ClickHouse's `Lexer.cpp` does for chained access (`x.1.1`). At 
this time, `t.1.2` parses as one `Dot(Number("1.2"))`, while `clickhouse local` 
returns `2` for `SELECT ((1, 2), 3).1.2`.
   
   I wonder whether there is a way for us to wrap maybe via ffi ClickHouse's 
lexer and directly fuzz against it, so we can have more targetted feedback.
   
   ```suggestion
                       // 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)));
                       }
   ```



##########
tests/sqlparser_clickhouse.rs:
##########
@@ -2022,6 +2022,40 @@ fn parse_object_type_parameter() {
     }
 }
 
+#[test]
+fn parse_tuple_element_access() {
+    clickhouse().verified_stmt("SELECT t.1 FROM t");
+    clickhouse().verified_stmt("SELECT t.1 AS a, t.2 AS b FROM (SELECT (1, 
'x') AS t)");
+    clickhouse().verified_stmt("SELECT (1, 'a').1");
+    clickhouse().verified_stmt("SELECT tuple(1, 'a').2");
+    clickhouse().verified_stmt("SELECT arr[1].1 FROM t");
+    clickhouse().verified_stmt("SELECT `t`.1 FROM t");
+    clickhouse().verified_stmt("SELECT t.1 + 1 FROM t");
+    clickhouse().verified_stmt("SELECT * FROM t WHERE t.1 = 1");
+
+    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()))],
+        })
+    );
+
+    clickhouse().one_statement_parses_to("SELECT t . 1 FROM t", "SELECT t.1 
FROM t");

Review Comment:
   You should expect the ` . ` canonical form that follows from the previous 
note.
   
   ```suggestion
       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",
           ),
       ] {
           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()))],
           })
       );
   ```



##########
tests/sqlparser_clickhouse.rs:
##########
@@ -2022,6 +2022,40 @@ fn parse_object_type_parameter() {
     }
 }
 
+#[test]
+fn parse_tuple_element_access() {
+    clickhouse().verified_stmt("SELECT t.1 FROM t");
+    clickhouse().verified_stmt("SELECT t.1 AS a, t.2 AS b FROM (SELECT (1, 
'x') AS t)");
+    clickhouse().verified_stmt("SELECT (1, 'a').1");
+    clickhouse().verified_stmt("SELECT tuple(1, 'a').2");
+    clickhouse().verified_stmt("SELECT arr[1].1 FROM t");
+    clickhouse().verified_stmt("SELECT `t`.1 FROM t");
+    clickhouse().verified_stmt("SELECT t.1 + 1 FROM t");
+    clickhouse().verified_stmt("SELECT * FROM t WHERE t.1 = 1");
+
+    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()))],
+        })
+    );
+
+    clickhouse().one_statement_parses_to("SELECT t . 1 FROM t", "SELECT t.1 
FROM t");
+
+    clickhouse().verified_stmt("SELECT 1.5, 1 + 0.5");

Review Comment:
   Red test for the chained access.
   
   ```suggestion
       clickhouse().verified_stmt("SELECT 1.5, 1 + 0.5");
       clickhouse().one_statement_parses_to("SELECT t.1.2 FROM t", "SELECT t . 
1 . 2 FROM t");
   ```



##########
src/ast/mod.rs:
##########
@@ -1758,8 +1758,29 @@ impl fmt::Display for Expr {
             Expr::CompoundIdentifier(s) => write!(f, "{}", 
display_separated(s, ".")),
             Expr::CompoundFieldAccess { root, access_chain } => {
                 write!(f, "{root}")?;
+                // `.1` tokenizes as a period only after an identifier, `)` or 
`]`.

Review Comment:
   Likely your should revert this and keep main's ` . 1` rendering, because 
`Display` does not know the dialect and every dialect without the flag lexes 
`.1` as a number. `SELECT (1, 'a') . 1` parses in Generic, PostgreSQL, 
Snowflake and BigQuery, renders as `SELECT (1, 'a').1` and fails to parse back 
(#2463 added the spacing for this reason). ClickHouse parses `t . 1` fine.



##########
tests/sqlparser_clickhouse.rs:
##########
@@ -2022,6 +2022,40 @@ fn parse_object_type_parameter() {
     }
 }
 
+#[test]
+fn parse_tuple_element_access() {
+    clickhouse().verified_stmt("SELECT t.1 FROM t");
+    clickhouse().verified_stmt("SELECT t.1 AS a, t.2 AS b FROM (SELECT (1, 
'x') AS t)");
+    clickhouse().verified_stmt("SELECT (1, 'a').1");
+    clickhouse().verified_stmt("SELECT tuple(1, 'a').2");
+    clickhouse().verified_stmt("SELECT arr[1].1 FROM t");
+    clickhouse().verified_stmt("SELECT `t`.1 FROM t");
+    clickhouse().verified_stmt("SELECT t.1 + 1 FROM t");
+    clickhouse().verified_stmt("SELECT * FROM t WHERE t.1 = 1");
+
+    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()))],
+        })
+    );
+
+    clickhouse().one_statement_parses_to("SELECT t . 1 FROM t", "SELECT t.1 
FROM t");
+
+    clickhouse().verified_stmt("SELECT 1.5, 1 + 0.5");
+
+    assert!(clickhouse().parse_sql_statements("SELECT t.").is_err());
+
+    let unsupported = all_dialects_where(|d| 
!d.supports_tuple_element_access());

Review Comment:
   Red test for the round trip for dialects without the flag. It fails on this 
head with `right: "SELECT (1, 'a').1"`.
   
   ```suggestion
       let unsupported = all_dialects_where(|d| 
!d.supports_tuple_element_access());
       unsupported.verified_stmt("SELECT (1, 'a') . 1");
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to