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

Issue:
Add the support of column TTL parsing
This is a simple example, for more details, can refer to https://clickhouse.com/docs/guides/developer/ttl.
```sql
CREATE TABLE example1 (
   timestamp DateTime,
   x UInt32 TTL timestamp + INTERVAL 1 MONTH,
   y String TTL timestamp + INTERVAL 1 DAY,
   z String
)
ENGINE = MergeTree
ORDER BY tuple()

```

Pull request description:
Add support of TTL expression in column definition
This closes #135.

Patch:
diff --git a/parser/ast.go b/parser/ast.go
index 37f0282..c3206a0 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -3220,7 +3220,7 @@ type ColumnDef struct {
 	AliasExpr        Expr
 
 	Codec *CompressionCodec
-	TTL   Expr
+	TTL   *TTLClause
 
 	Comment          *StringLiteral
 	CompressionCodec *Ident
diff --git a/parser/parser_table.go b/parser/parser_table.go
index 7bbab20..56ee15a 100644
--- a/parser/parser_table.go
+++ b/parser/parser_table.go
@@ -482,6 +482,14 @@ func (p *Parser) parseTableColumnExpr(pos Pos) (*ColumnDef, error) {
 	if codec != nil {
 		columnEnd = codec.End()
 	}
+	ttl, err := p.tryParseTTLClause(p.Pos(), false)
+	if err != nil {
+		return nil, err
+	}
+	if ttl != nil {
+		columnEnd = ttl.End()
+	}
+	column.TTL = ttl
 
 	column.ColumnEnd = columnEnd
 	column.Comment = comment
@@ -702,13 +710,13 @@ func (p *Parser) parseOrderExpr(pos Pos) (*OrderExpr, error) {
 	}, nil
 }
 
-func (p *Parser) tryParseTTLClause(pos Pos) (*TTLClause, error) {
+func (p *Parser) tryParseTTLClause(pos Pos, allowMultiValues bool) (*TTLClause, error) {
 	if p.tryConsumeKeyword(KeywordTtl) == nil {
 		return nil, nil // nolint
 	}
 	ttlExprList := &TTLClause{TTLPos: pos, ListEnd: pos}
 	// accept the TTL keyword
-	items, err := p.parseTTLClause(pos)
+	items, err := p.parseTTLClause(pos, allowMultiValues)
 	if err != nil {
 		return nil, err
 	}
@@ -719,14 +727,16 @@ func (p *Parser) tryParseTTLClause(pos Pos) (*TTLClause, error) {
 	return ttlExprList, nil
 }
 
-func (p *Parser) parseTTLClause(pos Pos) ([]*TTLExpr, error) {
+// parseTTLClause parses the TTL clause.
+// allowMultiValues is used to determine whether to allow multiple TTL values.
+func (p *Parser) parseTTLClause(pos Pos, allowMultiValues bool) ([]*TTLExpr, error) {
 	items := make([]*TTLExpr, 0)
 	expr, err := p.parseTTLExpr(pos)
 	if err != nil {
 		return nil, err
 	}
 	items = append(items, expr)
-	for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindComma) != nil {
+	for allowMultiValues && !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindComma) != nil {
 		expr, err = p.parseTTLExpr(pos)
 		if err != nil {
 			return nil, err
@@ -932,7 +942,7 @@ func (p *Parser) parseEngineExpr(pos Pos) (*EngineExpr, error) {
 			engineExpr.SampleBy = sampleBy
 			engineEnd = sampleBy.End()
 		case p.matchKeyword(KeywordTtl):
-			ttl, err := p.tryParseTTLClause(p.Pos())
+			ttl, err := p.tryParseTTLClause(p.Pos(), true)
 			if err != nil {
 				return nil, err
 			}


Test patch:
diff --git a/parser/testdata/dml/create_column_with_ttl.sql b/parser/testdata/dml/create_column_with_ttl.sql
new file mode 100644
index 0000000..f48418c
--- /dev/null
+++ b/parser/testdata/dml/create_column_with_ttl.sql
@@ -0,0 +1,7 @@
+CREATE TABLE example1 (
+    timestamp DateTime,
+    x UInt32 TTL timestamp + INTERVAL 1 MONTH,
+    y UInt32 TTL timestamp + INTERVAL 1 WEEK
+)
+ENGINE = MergeTree
+ORDER BY tuple()
\ No newline at end of file
diff --git a/parser/testdata/dml/format/create_column_with_ttl.sql b/parser/testdata/dml/format/create_column_with_ttl.sql
new file mode 100644
index 0000000..1a7cc0e
--- /dev/null
+++ b/parser/testdata/dml/format/create_column_with_ttl.sql
@@ -0,0 +1,11 @@
+-- Origin SQL:
+CREATE TABLE example1 (
+    timestamp DateTime,
+    x UInt32 TTL timestamp + INTERVAL 1 MONTH,
+    y UInt32 TTL timestamp + INTERVAL 1 WEEK
+)
+ENGINE = MergeTree
+ORDER BY tuple()
+
+-- Format SQL:
+CREATE TABLE example1 (timestamp DateTime, x UInt32 TTL timestamp + INTERVAL 1 MONTH, y UInt32 TTL timestamp + INTERVAL 1 WEEK) ENGINE = MergeTree ORDER BY tuple();
diff --git a/parser/testdata/dml/output/create_column_with_ttl.sql.golden.json b/parser/testdata/dml/output/create_column_with_ttl.sql.golden.json
new file mode 100644
index 0000000..64babed
--- /dev/null
+++ b/parser/testdata/dml/output/create_column_with_ttl.sql.golden.json
@@ -0,0 +1,228 @@
+[
+  {
+    "CreatePos": 0,
+    "StatementEnd": 176,
+    "Name": {
+      "Database": null,
+      "Table": {
+        "Name": "example1",
+        "QuoteType": 1,
+        "NamePos": 13,
+        "NameEnd": 21
+      }
+    },
+    "IfNotExists": false,
+    "UUID": null,
+    "OnCluster": null,
+    "TableSchema": {
+      "SchemaPos": 22,
+      "SchemaEnd": 140,
+      "Columns": [
+        {
+          "NamePos": 28,
+          "ColumnEnd": 46,
+          "Name": {
+            "Ident": {
+              "Name": "timestamp",
+              "QuoteType": 1,
+              "NamePos": 28,
+              "NameEnd": 37
+            },
+            "DotIdent": null
+          },
+          "Type": {
+            "Name": {
+              "Name": "DateTime",
+              "QuoteType": 1,
+              "NamePos": 38,
+              "NameEnd": 46
+            }
+          },
+          "NotNull": null,
+          "Nullable": null,
+          "DefaultExpr": null,
+          "MaterializedExpr": null,
+          "AliasExpr": null,
+          "Codec": null,
+          "TTL": null,
+          "Comment": null,
+          "CompressionCodec": null
+        },
+        {
+          "NamePos": 52,
+          "ColumnEnd": 93,
+          "Name": {
+            "Ident": {
+              "Name": "x",
+              "QuoteType": 1,
+              "NamePos": 52,
+              "NameEnd": 53
+            },
+            "DotIdent": null
+          },
+          "Type": {
+            "Name": {
+              "Name": "UInt32",
+              "QuoteType": 1,
+              "NamePos": 54,
+              "NameEnd": 60
+            }
+          },
+          "NotNull": null,
+          "Nullable": null,
+          "DefaultExpr": null,
+          "MaterializedExpr": null,
+          "AliasExpr": null,
+          "Codec": null,
+          "TTL": {
+            "TTLPos": 61,
+            "ListEnd": 93,
+            "Items": [
+              {
+                "TTLPos": 61,
+                "Expr": {
+                  "LeftExpr": {
+                    "Name": "timestamp",
+                    "QuoteType": 1,
+                    "NamePos": 65,
+                    "NameEnd": 74
+                  },
+                  "Operation": "+",
+                  "RightExpr": {
+                    "IntervalPos": 77,
+                    "Expr": {
+                      "NumPos": 86,
+                      "NumEnd": 87,
+                      "Literal": "1",
+                      "Base": 10
+                    },
+                    "Unit": {
+                      "Name": "MONTH",
+                      "QuoteType": 1,
+                      "NamePos": 88,
+                      "NameEnd": 93
+                    }
+                  },
+                  "HasGlobal": false,
+                  "HasNot": false
+                }
+              }
+            ]
+          },
+          "Comment": null,
+          "CompressionCodec": null
+        },
+        {
+          "NamePos": 99,
+          "ColumnEnd": 139,
+          "Name": {
+            "Ident": {
+              "Name": "y",
+              "QuoteType": 1,
+              "NamePos": 99,
+              "NameEnd": 100
+            },
+            "DotIdent": null
+          },
+          "Type": {
+            "Name": {
+              "Name": "UInt32",
+              "QuoteType": 1,
+              "NamePos": 101,
+              "NameEnd": 107
+            }
+          },
+          "NotNull": null,
+          "Nullable": null,
+          "DefaultExpr": null,
+          "MaterializedExpr": null,
+          "AliasExpr": null,
+          "Codec": null,
+          "TTL": {
+            "TTLPos": 108,
+            "ListEnd": 139,
+            "Items": [
+              {
+                "TTLPos": 108,
+                "Expr": {
+                  "LeftExpr": {
+                    "Name": "timestamp",
+                    "QuoteType": 1,
+                    "NamePos": 112,
+                    "NameEnd": 121
+                  },
+                  "Operation": "+",
+                  "RightExpr": {
+                    "IntervalPos": 124,
+                    "Expr": {
+                      "NumPos": 133,
+                      "NumEnd": 134,
+                      "Literal": "1",
+                      "Base": 10
+                    },
+                    "Unit": {
+                      "Name": "WEEK",
+                      "QuoteType": 1,
+                      "NamePos": 135,
+                      "NameEnd": 139
+                    }
+                  },
+                  "HasGlobal": false,
+                  "HasNot": false
+                }
+              }
+            ]
+          },
+          "Comment": null,
+          "CompressionCodec": null
+        }
+      ],
+      "AliasTable": null,
+      "TableFunction": null
+    },
+    "Engine": {
+      "EnginePos": 142,
+      "EngineEnd": 176,
+      "Name": "MergeTree",
+      "Params": null,
+      "PrimaryKey": null,
+      "PartitionBy": null,
+      "SampleBy": null,
+      "TTL": null,
+      "Settings": null,
+      "OrderBy": {
+        "OrderPos": 161,
+        "ListEnd": 176,
+        "Items": [
+          {
+            "OrderPos": 161,
+            "Expr": {
+              "Name": {
+                "Name": "tuple",
+                "QuoteType": 1,
+                "NamePos": 170,
+                "NameEnd": 175
+              },
+              "Params": {
+                "LeftParenPos": 175,
+                "RightParenPos": 176,
+                "Items": {
+                  "ListPos": 176,
+                  "ListEnd": 176,
+                  "HasDistinct": false,
+                  "Items": []
+                },
+                "ColumnArgList": null
+              }
+            },
+            "Alias": null,
+            "Direction": "None"
+          }
+        ]
+      }
+    },
+    "SubQuery": null,
+    "HasTemporary": false,
+    "Comment": null
+  }
+]
\ No newline at end of file