Repository: AfterShip/clickhouse-sql-parser (go)

Issue:
Accessing tuples with dot syntax is not supported
In ClickHouse Tuple values may be accessed by using dot notation:
```
SELECT tuple('a','b','c').3 --> 'c'
```
further tuple elements can have names and accessed by the name. A more elaborate example:
```
SELECT toTypeName( tuple('a' as first,'b' as second ,'c' as third)::Tuple(first String,second String,third String)),
       (tuple('a' as first,'b' as second ,'c' as third)::Tuple(first String,second String,third String)).second,
       tuple('a','b','c').3,
       tupleElement(tuple('a','b','c'),1)
```
It would IMHO improve this library if it would support the tuple dot notation.

Pull request description:
Add support of accessing the tuple with index field
This closes #113 

Patch:
diff --git a/parser/ast.go b/parser/ast.go
index aff1ff1..eb62bfa 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -187,6 +187,40 @@ func (p *BinaryOperation) Accept(visitor ASTVisitor) error {
 	return visitor.VisitBinaryExpr(p)
 }
 
+type IndexOperation struct {
+	LeftExpr  Expr
+	Operation TokenKind
+	Index     Expr
+}
+
+func (i *IndexOperation) Accept(visitor ASTVisitor) error {
+	visitor.enter(i)
+	defer visitor.leave(i)
+	if err := i.LeftExpr.Accept(visitor); err != nil {
+		return err
+	}
+	if err := i.Index.Accept(visitor); err != nil {
+		return err
+	}
+	return visitor.VisitIndexOperation(i)
+}
+
+func (i *IndexOperation) Pos() Pos {
+	return i.LeftExpr.Pos()
+}
+
+func (i *IndexOperation) End() Pos {
+	return i.Index.End()
+}
+
+func (i *IndexOperation) String() string {
+	var builder strings.Builder
+	builder.WriteString(i.LeftExpr.String())
+	builder.WriteString(string(i.Operation))
+	builder.WriteString(i.Index.String())
+	return builder.String()
+}
+
 type JoinTableExpr struct {
 	Table        *TableExpr
 	StatementEnd Pos
diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go
index a06bf3c..5595204 100644
--- a/parser/ast_visitor.go
+++ b/parser/ast_visitor.go
@@ -4,6 +4,7 @@ type ASTVisitor interface {
 	VisitOperationExpr(expr *OperationExpr) error
 	VisitTernaryExpr(expr *TernaryOperation) error
 	VisitBinaryExpr(expr *BinaryOperation) error
+	VisitIndexOperation(expr *IndexOperation) error
 	VisitAlterTable(expr *AlterTable) error
 	VisitAlterTableAttachPartition(expr *AlterTableAttachPartition) error
 	VisitAlterTableDetachPartition(expr *AlterTableDetachPartition) error
@@ -188,6 +189,13 @@ func (v *DefaultASTVisitor) VisitBinaryExpr(expr *BinaryOperation) error {
 	return nil
 }
 
+func (v *DefaultASTVisitor) VisitIndexOperation(expr *IndexOperation) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
 func (v *DefaultASTVisitor) VisitJoinTableExpr(expr *JoinTableExpr) error {
 	if v.Visit != nil {
 		return v.Visit(expr)
diff --git a/parser/lexer.go b/parser/lexer.go
index 4de4d54..1f2654d 100644
--- a/parser/lexer.go
+++ b/parser/lexer.go
@@ -15,6 +15,7 @@ const (
 	TokenInt     TokenKind = "<int>"
 	TokenFloat   TokenKind = "<float>"
 	TokenString  TokenKind = "<string>"
+	TokenDot               = "."
 )
 
 const (
@@ -328,19 +329,14 @@ func (l *Lexer) consumeToken() error {
 			return nil
 		}
 	case '.':
-		// check if the next token is a number. If so, parse it as a float number
-		if l.peekOk(1) && IsDigit(l.peekN(1)) {
-			return l.consumeNumber()
-		}
-		// check if the previous lastToken is an Ident. If so, it's a field name.
-		if l.lastToken != nil && l.lastToken.Kind != TokenIdent {
-			return fmt.Errorf("'.' should be after an Ident, but got <%q>", l.lastToken.Kind)
+		l.lastToken = &Token{
+			String: l.slice(0, 1),
+			Kind:   TokenDot,
+			Pos:    Pos(l.current),
+			End:    Pos(l.current + 1),
 		}
-	}
-
-	// The subsequent lastToken after the dot should be an Ident.
-	if l.lastToken != nil && l.lastToken.Kind == "." && !IsIdentStart(l.peekN(0)) {
-		return fmt.Errorf("'.' should follow with an Ident, but got <%q>", l.lastToken.Kind)
+		l.skipN(1)
+		return nil
 	}
 
 	if IsIdentStart(l.peekN(0)) {
diff --git a/parser/parse_system.go b/parser/parse_system.go
index 817e82f..9974450 100644
--- a/parser/parse_system.go
+++ b/parser/parse_system.go
@@ -921,7 +921,7 @@ func (p *Parser) parseGrantSource(_ Pos) (*TableIdentifier, error) {
 		return nil, err
 	}
 
-	if p.tryConsumeTokenKind(".") == nil {
+	if p.tryConsumeTokenKind(TokenDot) == nil {
 		return &TableIdentifier{
 			Table: ident,
 		}, nil
diff --git a/parser/parser_column.go b/parser/parser_column.go
index be18de0..cef786a 100644
--- a/parser/parser_column.go
+++ b/parser/parser_column.go
@@ -20,6 +20,7 @@ const (
 	PrecedenceMulDivMod
 	PrecedenceBracket
 	PrecedenceArrow
+	PrecedenceDot
 	PrecedenceDoubleColon
 )
 
@@ -56,6 +57,8 @@ func (p *Parser) getNextPrecedence() int {
 		return PrecedenceBracket
 	case p.matchTokenKind(opTypeCast):
 		return PrecedenceDoubleColon
+	case p.matchTokenKind(TokenDot):
+		return PrecedenceDot
 	case p.matchKeyword(KeywordBetween), p.matchKeyword(KeywordLike), p.matchKeyword(KeywordIlike):
 		return PrecedenceBetweenLike
 	case p.matchKeyword(KeywordIn):
@@ -78,7 +81,7 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) {
 		p.matchTokenKind(opTypeDiv), p.matchTokenKind(opTypeMod),
 		p.matchKeyword(KeywordIn), p.matchKeyword(KeywordLike),
 		p.matchKeyword(KeywordIlike), p.matchKeyword(KeywordAnd), p.matchKeyword(KeywordOr),
-		p.matchTokenKind(opTypeCast), p.matchTokenKind(opTypeArrow), p.matchTokenKind(opTypeDoubleEQ):
+		p.matchTokenKind(opTypeArrow), p.matchTokenKind(opTypeDoubleEQ):
 		op := p.last().ToString()
 		_ = p.lexer.consumeToken()
 		rightExpr, err := p.parseSubExpr(p.Pos(), precedence)
@@ -90,6 +93,38 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) {
 			Operation: TokenKind(op),
 			RightExpr: rightExpr,
 		}, nil
+	case p.matchTokenKind(opTypeCast):
+		_ = p.lexer.consumeToken()
+
+		if p.matchTokenKind(TokenIdent) && p.last().String == "Tuple" {
+			name, err := p.parseIdent()
+			if err != nil {
+				return nil, err
+			}
+			if _, err = p.consumeTokenKind("("); err != nil {
+				return nil, err
+			}
+			// it's a tuple type definition after "::" operator
+			rightExpr, err := p.parseNestedType(name, p.Pos())
+			if err != nil {
+				return nil, err
+			}
+			return &BinaryOperation{
+				LeftExpr:  expr,
+				Operation: opTypeCast,
+				RightExpr: rightExpr,
+			}, nil
+		}
+
+		rightExpr, err := p.parseSubExpr(p.Pos(), precedence)
+		if err != nil {
+			return nil, err
+		}
+		return &BinaryOperation{
+			LeftExpr:  expr,
+			Operation: opTypeCast,
+			RightExpr: rightExpr,
+		}, nil
 	case p.matchKeyword(KeywordBetween):
 		return p.parseBetweenClause(expr)
 	case p.matchKeyword(KeywordGlobal):
@@ -106,7 +141,24 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) {
 			Operation: "GLOBAL IN",
 			RightExpr: rightExpr,
 		}, nil
-
+	case p.matchTokenKind(TokenDot):
+		_ = p.lexer.consumeToken()
+		// access column with dot notation
+		var rightExpr Expr
+		var err error
+		if p.matchTokenKind(TokenIdent) {
+			rightExpr, err = p.parseIdent()
+		} else {
+			rightExpr, err = p.parseDecimal(p.Pos())
+		}
+		if err != nil {
+			return nil, err
+		}
+		return &IndexOperation{
+			LeftExpr:  expr,
+			Operation: TokenDot,
+			Index:     rightExpr,
+		}, nil
 	case p.matchKeyword(KeywordNot):
 		_ = p.lexer.consumeToken()
 		switch {
@@ -331,6 +383,8 @@ func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen
 			return p.parseQueryParam(p.Pos())
 		}
 		return p.parseMapLiteral(p.Pos())
+	case p.matchTokenKind(TokenDot):
+		return p.parseNumber(p.Pos())
 	case p.matchTokenKind(opTypeQuery):
 		// Placeholder `?`
 		_ = p.lexer.consumeToken()
diff --git a/parser/parser_common.go b/parser/parser_common.go
index 5f21e30..c5e148c 100644
--- a/parser/parser_common.go
+++ b/parser/parser_common.go
@@ -110,7 +110,7 @@ func (p *Parser) parseIdentOrStar() (*Ident, error) {
 }
 
 func (p *Parser) tryParseDotIdent(_ Pos) (*Ident, error) {
-	if p.tryConsumeTokenKind(".") == nil {
+	if p.tryConsumeTokenKind(TokenDot) == nil {
 		return nil, nil // nolint
 	}
 	return p.parseIdent()
@@ -211,6 +211,17 @@ func (p *Parser) parseNumber(pos Pos) (*NumberLiteral, error) {
 		lastToken, err = p.consumeTokenKind(TokenInt)
 	case p.matchTokenKind(TokenFloat):
 		lastToken, err = p.consumeTokenKind(TokenFloat)
+	case p.matchTokenKind(TokenDot):
+		_ = p.lexer.consumeToken()
+		lastToken, err = p.consumeTokenKind(TokenInt)
+		if err != nil {
+			return nil, err
+		}
+		if lastToken.Base != 10 {
+			return nil, fmt.Errorf("invalid decimal literal: %q", lastToken.String)
+		}
+		lastToken.String = "." + lastToken.String
+		lastToken.Kind = TokenFloat
 	default:
 		return nil, fmt.Errorf("expected <int> or <float>, but got %q", p.lastTokenKind())
 	}
diff --git a/parser/parser_table.go b/parser/parser_table.go
index 2e4e92f..718c1ae 100644
--- a/parser/parser_table.go
+++ b/parser/parser_table.go
@@ -223,14 +223,14 @@ func (p *Parser) parseIdentOrFunction(_ Pos) (Expr, error) {
 			}, nil
 		}
 		return funcExpr, nil
-	case p.tryConsumeTokenKind(".") != nil:
+	case p.tryConsumeTokenKind(TokenDot) != nil:
 		switch {
 		case p.matchTokenKind(TokenIdent):
 			nextIdent, err := p.parseIdent()
 			if err != nil {
 				return nil, err
 			}
-			if p.tryConsumeTokenKind(".") != nil {
+			if p.tryConsumeTokenKind(TokenDot) != nil {
 				thirdIdent, err := p.parseIdent()
 				if err != nil {
 					return nil, err
@@ -310,7 +310,7 @@ func (p *Parser) parseTableSchemaClause(pos Pos) (*TableSchemaClause, error) {
 				return nil, err
 			}
 			switch {
-			case p.matchTokenKind("."):
+			case p.matchTokenKind(TokenDot):
 				// it's a database.table
 				dotIdent, err := p.tryParseDotIdent(p.Pos())
 				if err != nil {
@@ -489,7 +489,7 @@ func (p *Parser) parseTableArgExpr(pos Pos) (Expr, error) {
 		}
 		switch {
 		// nest identifier
-		case p.matchTokenKind("."):
+		case p.matchTokenKind(TokenDot):
 			dotIdent, err := p.tryParseDotIdent(p.Pos())
 			if err != nil {
 				return nil, err


Test patch:
diff --git a/parser/lexer_test.go b/parser/lexer_test.go
index ce5a4a1..addc963 100644
--- a/parser/lexer_test.go
+++ b/parser/lexer_test.go
@@ -106,7 +106,6 @@ func TestConsumeNumber(t *testing.T) {
 	t.Run("Float number", func(t *testing.T) {
 		floats := []string{
 			"123.456",
-			".456",
 			"123.456e+10",
 			"123.456e-10",
 			"123.456e10",
@@ -126,7 +125,6 @@ func TestConsumeNumber(t *testing.T) {
 
 	t.Run("Invalid float number", func(t *testing.T) {
 		invalidFloats := []string{
-			".456a",
 			"123.456b",
 			"123.456e",
 			"123.456e+",
diff --git a/parser/testdata/query/access_tuple_with_dot.sql b/parser/testdata/query/access_tuple_with_dot.sql
new file mode 100644
index 0000000..38b1184
--- /dev/null
+++ b/parser/testdata/query/access_tuple_with_dot.sql
@@ -0,0 +1,6 @@
+SELECT tuple('a','b','c').3, .1234;
+
+SELECT toTypeName( tuple('a' as first,'b' as second ,'c' as third)::Tuple(first String,second String,third String)),
+       (tuple('a' as first,'b' as second ,'c' as third)::Tuple(first String,second String,third String)).second,
+       tuple('a','b','c').3,
+       tupleElement(tuple('a','b','c'),1)
\ No newline at end of file
diff --git a/parser/testdata/query/format/access_tuple_with_dot.sql b/parser/testdata/query/format/access_tuple_with_dot.sql
new file mode 100644
index 0000000..e7672c9
--- /dev/null
+++ b/parser/testdata/query/format/access_tuple_with_dot.sql
@@ -0,0 +1,11 @@
+-- Origin SQL:
+SELECT tuple('a','b','c').3, .1234;
+
+SELECT toTypeName( tuple('a' as first,'b' as second ,'c' as third)::Tuple(first String,second String,third String)),
+       (tuple('a' as first,'b' as second ,'c' as third)::Tuple(first String,second String,third String)).second,
+       tuple('a','b','c').3,
+       tupleElement(tuple('a','b','c'),1)
+
+-- Format SQL:
+SELECT tuple('a', 'b', 'c').3, .1234;
+SELECT toTypeName(tuple('a' AS first, 'b' AS second, 'c' AS third)::Tuple(first String, second String, third String)), (tuple('a' AS first, 'b' AS second, 'c' AS third)::Tuple(first String, second String, third String)).second, tuple('a', 'b', 'c').3, tupleElement(tuple('a', 'b', 'c'), 1);
diff --git a/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json b/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json
new file mode 100644
index 0000000..563e409
--- /dev/null
+++ b/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json
@@ -0,0 +1,641 @@
+[
+  {
+    "SelectPos": 0,
+    "StatementEnd": 34,
+    "With": null,
+    "Top": null,
+    "SelectItems": [
+      {
+        "Expr": {
+          "LeftExpr": {
+            "Name": {
+              "Name": "tuple",
+              "QuoteType": 1,
+              "NamePos": 7,
+              "NameEnd": 12
+            },
+            "Params": {
+              "LeftParenPos": 12,
+              "RightParenPos": 24,
+              "Items": {
+                "ListPos": 14,
+                "ListEnd": 23,
+                "HasDistinct": false,
+                "Items": [
+                  {
+                    "Expr": {
+                      "LiteralPos": 14,
+                      "LiteralEnd": 15,
+                      "Literal": "a"
+                    },
+                    "Alias": null
+                  },
+                  {
+                    "Expr": {
+                      "LiteralPos": 18,
+                      "LiteralEnd": 19,
+                      "Literal": "b"
+                    },
+                    "Alias": null
+                  },
+                  {
+                    "Expr": {
+                      "LiteralPos": 22,
+                      "LiteralEnd": 23,
+                      "Literal": "c"
+                    },
+                    "Alias": null
+                  }
+                ]
+              },
+              "ColumnArgList": null
+            }
+          },
+          "Operation": ".",
+          "Index": {
+            "NumPos": 26,
+            "NumEnd": 27,
+            "Literal": "3",
+            "Base": 10
+          }
+        },
+        "Modifiers": [],
+        "Alias": null
+      },
+      {
+        "Expr": {
+          "NumPos": 29,
+          "NumEnd": 34,
+          "Literal": ".1234",
+          "Base": 10
+        },
+        "Modifiers": [],
+        "Alias": null
+      }
+    ],
+    "From": null,
+    "ArrayJoin": null,
+    "Window": null,
+    "Prewhere": null,
+    "Where": null,
+    "GroupBy": null,
+    "WithTotal": false,
+    "Having": null,
+    "OrderBy": null,
+    "LimitBy": null,
+    "Limit": null,
+    "Settings": null,
+    "Format": null,
+    "UnionAll": null,
+    "UnionDistinct": null,
+    "Except": null
+  },
+  {
+    "SelectPos": 37,
+    "StatementEnd": 336,
+    "With": null,
+    "Top": null,
+    "SelectItems": [
+      {
+        "Expr": {
+          "Name": {
+            "Name": "toTypeName",
+            "QuoteType": 1,
+            "NamePos": 44,
+            "NameEnd": 54
+          },
+          "Params": {
+            "LeftParenPos": 54,
+            "RightParenPos": 151,
+            "Items": {
+              "ListPos": 56,
+              "ListEnd": 150,
+              "HasDistinct": false,
+              "Items": [
+                {
+                  "Expr": {
+                    "LeftExpr": {
+                      "Name": {
+                        "Name": "tuple",
+                        "QuoteType": 1,
+                        "NamePos": 56,
+                        "NameEnd": 61
+                      },
+                      "Params": {
+                        "LeftParenPos": 61,
+                        "RightParenPos": 102,
+                        "Items": {
+                          "ListPos": 63,
+                          "ListEnd": 102,
+                          "HasDistinct": false,
+                          "Items": [
+                            {
+                              "Expr": {
+                                "LiteralPos": 63,
+                                "LiteralEnd": 64,
+                                "Literal": "a"
+                              },
+                              "Alias": {
+                                "Name": "first",
+                                "QuoteType": 1,
+                                "NamePos": 69,
+                                "NameEnd": 74
+                              }
+                            },
+                            {
+                              "Expr": {
+                                "LiteralPos": 76,
+                                "LiteralEnd": 77,
+                                "Literal": "b"
+                              },
+                              "Alias": {
+                                "Name": "second",
+                                "QuoteType": 1,
+                                "NamePos": 82,
+                                "NameEnd": 88
+                              }
+                            },
+                            {
+                              "Expr": {
+                                "LiteralPos": 91,
+                                "LiteralEnd": 92,
+                                "Literal": "c"
+                              },
+                              "Alias": {
+                                "Name": "third",
+                                "QuoteType": 1,
+                                "NamePos": 97,
+                                "NameEnd": 102
+                              }
+                            }
+                          ]
+                        },
+                        "ColumnArgList": null
+                      }
+                    },
+                    "Operation": "::",
+                    "RightExpr": {
+                      "LeftParenPos": 111,
+                      "RightParenPos": 150,
+                      "Name": {
+                        "Name": "Tuple",
+                        "QuoteType": 1,
+                        "NamePos": 105,
+                        "NameEnd": 110
+                      },
+                      "Columns": [
+                        {
+                          "NamePos": 111,
+                          "ColumnEnd": 123,
+                          "Name": {
+                            "Ident": {
+                              "Name": "first",
+                              "QuoteType": 1,
+                              "NamePos": 111,
+                              "NameEnd": 116
+                            },
+                            "DotIdent": null
+                          },
+                          "Type": {
+                            "Name": {
+                              "Name": "String",
+                              "QuoteType": 1,
+                              "NamePos": 117,
+                              "NameEnd": 123
+                            }
+                          },
+                          "NotNull": null,
+                          "Nullable": null,
+                          "DefaultExpr": null,
+                          "MaterializedExpr": null,
+                          "AliasExpr": null,
+                          "Codec": null,
+                          "TTL": null,
+                          "Comment": null,
+                          "CompressionCodec": null
+                        },
+                        {
+                          "NamePos": 124,
+                          "ColumnEnd": 137,
+                          "Name": {
+                            "Ident": {
+                              "Name": "second",
+                              "QuoteType": 1,
+                              "NamePos": 124,
+                              "NameEnd": 130
+                            },
+                            "DotIdent": null
+                          },
+                          "Type": {
+                            "Name": {
+                              "Name": "String",
+                              "QuoteType": 1,
+                              "NamePos": 131,
+                              "NameEnd": 137
+                            }
+                          },
+                          "NotNull": null,
+                          "Nullable": null,
+                          "DefaultExpr": null,
+                          "MaterializedExpr": null,
+                          "AliasExpr": null,
+                          "Codec": null,
+                          "TTL": null,
+                          "Comment": null,
+                          "CompressionCodec": null
+                        },
+                        {
+                          "NamePos": 138,
+                          "ColumnEnd": 150,
+                          "Name": {
+                            "Ident": {
+                              "Name": "third",
+                              "QuoteType": 1,
+                              "NamePos": 138,
+                              "NameEnd": 143
+                            },
+                            "DotIdent": null
+                          },
+                          "Type": {
+                            "Name": {
+                              "Name": "String",
+                              "QuoteType": 1,
+                              "NamePos": 144,
+                              "NameEnd": 150
+                            }
+                          },
+                          "NotNull": null,
+                          "Nullable": null,
+                          "DefaultExpr": null,
+                          "MaterializedExpr": null,
+                          "AliasExpr": null,
+                          "Codec": null,
+                          "TTL": null,
+                          "Comment": null,
+                          "CompressionCodec": null
+                        }
+                      ]
+                    },
+                    "HasGlobal": false,
+                    "HasNot": false
+                  },
+                  "Alias": null
+                }
+              ]
+            },
+            "ColumnArgList": null
+          }
+        },
+        "Modifiers": [],
+        "Alias": null
+      },
+      {
+        "Expr": {
+          "LeftExpr": {
+            "LeftParenPos": 161,
+            "RightParenPos": 257,
+            "Items": {
+              "ListPos": 162,
+              "ListEnd": 256,
+              "HasDistinct": false,
+              "Items": [
+                {
+                  "Expr": {
+                    "LeftExpr": {
+                      "Name": {
+                        "Name": "tuple",
+                        "QuoteType": 1,
+                        "NamePos": 162,
+                        "NameEnd": 167
+                      },
+                      "Params": {
+                        "LeftParenPos": 167,
+                        "RightParenPos": 208,
+                        "Items": {
+                          "ListPos": 169,
+                          "ListEnd": 208,
+                          "HasDistinct": false,
+                          "Items": [
+                            {
+                              "Expr": {
+                                "LiteralPos": 169,
+                                "LiteralEnd": 170,
+                                "Literal": "a"
+                              },
+                              "Alias": {
+                                "Name": "first",
+                                "QuoteType": 1,
+                                "NamePos": 175,
+                                "NameEnd": 180
+                              }
+                            },
+                            {
+                              "Expr": {
+                                "LiteralPos": 182,
+                                "LiteralEnd": 183,
+                                "Literal": "b"
+                              },
+                              "Alias": {
+                                "Name": "second",
+                                "QuoteType": 1,
+                                "NamePos": 188,
+                                "NameEnd": 194
+                              }
+                            },
+                            {
+                              "Expr": {
+                                "LiteralPos": 197,
+                                "LiteralEnd": 198,
+                                "Literal": "c"
+                              },
+                              "Alias": {
+                                "Name": "third",
+                                "QuoteType": 1,
+                                "NamePos": 203,
+                                "NameEnd": 208
+                              }
+                            }
+                          ]
+                        },
+                        "ColumnArgList": null
+                      }
+                    },
+                    "Operation": "::",
+                    "RightExpr": {
+                      "LeftParenPos": 217,
+                      "RightParenPos": 256,
+                      "Name": {
+                        "Name": "Tuple",
+                        "QuoteType": 1,
+                        "NamePos": 211,
+                        "NameEnd": 216
+                      },
+                      "Columns": [
+                        {
+                          "NamePos": 217,
+                          "ColumnEnd": 229,
+                          "Name": {
+                            "Ident": {
+                              "Name": "first",
+                              "QuoteType": 1,
+                              "NamePos": 217,
+                              "NameEnd": 222
+                            },
+                            "DotIdent": null
+                          },
+                          "Type": {
+                            "Name": {
+                              "Name": "String",
+                              "QuoteType": 1,
+                              "NamePos": 223,
+                              "NameEnd": 229
+                            }
+                          },
+                          "NotNull": null,
+                          "Nullable": null,
+                          "DefaultExpr": null,
+                          "MaterializedExpr": null,
+                          "AliasExpr": null,
+                          "Codec": null,
+                          "TTL": null,
+                          "Comment": null,
+                          "CompressionCodec": null
+                        },
+                        {
+                          "NamePos": 230,
+                          "ColumnEnd": 243,
+                          "Name": {
+                            "Ident": {
+                              "Name": "second",
+                              "QuoteType": 1,
+                              "NamePos": 230,
+                              "NameEnd": 236
+                            },
+                            "DotIdent": null
+                          },
+                          "Type": {
+                            "Name": {
+                              "Name": "String",
+                              "QuoteType": 1,
+                              "NamePos": 237,
+                              "NameEnd": 243
+                            }
+                          },
+                          "NotNull": null,
+                          "Nullable": null,
+                          "DefaultExpr": null,
+                          "MaterializedExpr": null,
+                          "AliasExpr": null,
+                          "Codec": null,
+                          "TTL": null,
+                          "Comment": null,
+                          "CompressionCodec": null
+                        },
+                        {
+                          "NamePos": 244,
+                          "ColumnEnd": 256,
+                          "Name": {
+                            "Ident": {
+                              "Name": "third",
+                              "QuoteType": 1,
+                              "NamePos": 244,
+                              "NameEnd": 249
+                            },
+                            "DotIdent": null
+                          },
+                          "Type": {
+                            "Name": {
+                              "Name": "String",
+                              "QuoteType": 1,
+                              "NamePos": 250,
+                              "NameEnd": 256
+                            }
+                          },
+                          "NotNull": null,
+                          "Nullable": null,
+                          "DefaultExpr": null,
+                          "MaterializedExpr": null,
+                          "AliasExpr": null,
+                          "Codec": null,
+                          "TTL": null,
+                          "Comment": null,
+                          "CompressionCodec": null
+                        }
+                      ]
+                    },
+                    "HasGlobal": false,
+                    "HasNot": false
+                  },
+                  "Alias": null
+                }
+              ]
+            },
+            "ColumnArgList": null
+          },
+          "Operation": ".",
+          "Index": {
+            "Name": "second",
+            "QuoteType": 1,
+            "NamePos": 259,
+            "NameEnd": 265
+          }
+        },
+        "Modifiers": [],
+        "Alias": null
+      },
+      {
+        "Expr": {
+          "LeftExpr": {
+            "Name": {
+              "Name": "tuple",
+              "QuoteType": 1,
+              "NamePos": 274,
+              "NameEnd": 279
+            },
+            "Params": {
+              "LeftParenPos": 279,
+              "RightParenPos": 291,
+              "Items": {
+                "ListPos": 281,
+                "ListEnd": 290,
+                "HasDistinct": false,
+                "Items": [
+                  {
+                    "Expr": {
+                      "LiteralPos": 281,
+                      "LiteralEnd": 282,
+                      "Literal": "a"
+                    },
+                    "Alias": null
+                  },
+                  {
+                    "Expr": {
+                      "LiteralPos": 285,
+                      "LiteralEnd": 286,
+                      "Literal": "b"
+                    },
+                    "Alias": null
+                  },
+                  {
+                    "Expr": {
+                      "LiteralPos": 289,
+                      "LiteralEnd": 290,
+                      "Literal": "c"
+                    },
+                    "Alias": null
+                  }
+                ]
+              },
+              "ColumnArgList": null
+            }
+          },
+          "Operation": ".",
+          "Index": {
+            "NumPos": 293,
+            "NumEnd": 294,
+            "Literal": "3",
+            "Base": 10
+          }
+        },
+        "Modifiers": [],
+        "Alias": null
+      },
+      {
+        "Expr": {
+          "Name": {
+            "Name": "tupleElement",
+            "QuoteType": 1,
+            "NamePos": 303,
+            "NameEnd": 315
+          },
+          "Params": {
+            "LeftParenPos": 315,
+            "RightParenPos": 336,
+            "Items": {
+              "ListPos": 316,
+              "ListEnd": 336,
+              "HasDistinct": false,
+              "Items": [
+                {
+                  "Expr": {
+                    "Name": {
+                      "Name": "tuple",
+                      "QuoteType": 1,
+                      "NamePos": 316,
+                      "NameEnd": 321
+                    },
+                    "Params": {
+                      "LeftParenPos": 321,
+                      "RightParenPos": 333,
+                      "Items": {
+                        "ListPos": 323,
+                        "ListEnd": 332,
+                        "HasDistinct": false,
+                        "Items": [
+                          {
+                            "Expr": {
+                              "LiteralPos": 323,
+                              "LiteralEnd": 324,
+                              "Literal": "a"
+                            },
+                            "Alias": null
+                          },
+                          {
+                            "Expr": {
+                              "LiteralPos": 327,
+                              "LiteralEnd": 328,
+                              "Literal": "b"
+                            },
+                            "Alias": null
+                          },
+                          {
+                            "Expr": {
+                              "LiteralPos": 331,
+                              "LiteralEnd": 332,
+                              "Literal": "c"
+                            },
+                            "Alias": null
+                          }
+                        ]
+                      },
+                      "ColumnArgList": null
+                    }
+                  },
+                  "Alias": null
+                },
+                {
+                  "Expr": {
+                    "NumPos": 335,
+                    "NumEnd": 336,
+                    "Literal": "1",
+                    "Base": 10
+                  },
+                  "Alias": null
+                }
+              ]
+            },
+            "ColumnArgList": null
+          }
+        },
+        "Modifiers": [],
+        "Alias": null
+      }
+    ],
+    "From": null,
+    "ArrayJoin": null,
+    "Window": null,
+    "Prewhere": null,
+    "Where": null,
+    "GroupBy": null,
+    "WithTotal": false,
+    "Having": null,
+    "OrderBy": null,
+    "LimitBy": null,
+    "Limit": null,
+    "Settings": null,
+    "Format": null,
+    "UnionAll": null,
+    "UnionDistinct": null,
+    "Except": null
+  }
+]
\ No newline at end of file