Repository: AfterShip/clickhouse-sql-parser
Pull request #151 (2025-04-23 07:12:53)

Add support of the typed placeholder parameters for INSERT statement
The sample can refer to:

```SQL
INSERT INTO TABLE test_with_typed_columns (id, created_at) VALUES ({id:Int32}, {created_at:DateTime64(6)});
```

coveralls: ## Pull Request Test Coverage Report for [Build 14612150965](https://coveralls.io/builds/73343268)


### Details

* **36** of **58**   **(62.07%)**  changed or added relevant lines in **3** files are covered.
* **593** unchanged lines in **1** file lost coverage.
* Overall coverage remained the same at **62.792%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [parser/ast_visitor.go](https://coveralls.io/builds/73343268/source?filename=parser%2Fast_visitor.go#L717) | 3 | 5 | 60.0%
| [parser/ast.go](https://coveralls.io/builds/73343268/source?filename=parser%2Fast.go#L3350) | 16 | 26 | 61.54%
| [parser/parser_table.go](https://coveralls.io/builds/73343268/source?filename=parser%2Fparser_table.go#L1207) | 17 | 27 | 62.96%
<!-- | **Total:** | **36** | **58** | **62.07%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [parser/ast.go](https://coveralls.io/builds/73343268/source?filename=parser%2Fast.go#L3389) | 593 | 58.4% |
<!-- | **Total:** | **593** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/73343268/badge)](https://coveralls.io/builds/73343268) |
| :-- | --: |
| Change from base [Build 14586084570](https://coveralls.io/builds/73321422): |  0.0% |
| Covered Lines: | 6936 |
| Relevant Lines: | 11046 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/parser/ast.go b/parser/ast.go
index 65bfbf7..90e23f3 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -3340,6 +3340,43 @@ func (w *WindowFunctionExpr) Accept(visitor ASTVisitor) error {
 	return visitor.VisitWindowFunctionExpr(w)
 }
 
+type TypedPlaceholder struct {
+	LeftBracePos  Pos
+	RightBracePos Pos
+	Name          *Ident
+	Type          ColumnType
+}
+
+func (t *TypedPlaceholder) Pos() Pos {
+	return t.LeftBracePos
+}
+
+func (t *TypedPlaceholder) End() Pos {
+	return t.RightBracePos
+}
+
+func (t *TypedPlaceholder) String() string {
+	var builder strings.Builder
+	builder.WriteString("{")
+	builder.WriteString(t.Name.String())
+	builder.WriteByte(':')
+	builder.WriteString(t.Type.String())
+	builder.WriteString("}")
+	return builder.String()
+}
+
+func (t *TypedPlaceholder) Accept(visitor ASTVisitor) error {
+	visitor.enter(t)
+	defer visitor.leave(t)
+	if err := t.Name.Accept(visitor); err != nil {
+		return err
+	}
+	if err := t.Type.Accept(visitor); err != nil {
+		return err
+	}
+	return visitor.VisitTypedPlaceholder(t)
+}
+
 type ColumnExpr struct {
 	Expr  Expr
 	Alias *Ident
diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go
index 8577bc4..06b8875 100644
--- a/parser/ast_visitor.go
+++ b/parser/ast_visitor.go
@@ -77,6 +77,7 @@ type ASTVisitor interface {
 	VisitWindowFunctionExpr(expr *WindowFunctionExpr) error
 	VisitColumnDef(expr *ColumnDef) error
 	VisitColumnExpr(expr *ColumnExpr) error
+	VisitTypedPlaceholder(expr *TypedPlaceholder) error
 	VisitScalarType(expr *ScalarType) error
 	VisitJSONType(expr *JSONType) error
 	VisitPropertyType(expr *PropertyType) error
@@ -711,6 +712,13 @@ func (v *DefaultASTVisitor) VisitColumnExpr(expr *ColumnExpr) error {
 	return nil
 }
 
+func (v *DefaultASTVisitor) VisitTypedPlaceholder(expr *TypedPlaceholder) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
 func (v *DefaultASTVisitor) VisitScalarType(expr *ScalarType) error {
 	if v.Visit != nil {
 		return v.Visit(expr)
diff --git a/parser/parser_table.go b/parser/parser_table.go
index bd012b5..e684bb4 100644
--- a/parser/parser_table.go
+++ b/parser/parser_table.go
@@ -1202,6 +1202,34 @@ func (p *Parser) parseColumnNamesExpr(pos Pos) (*ColumnNamesExpr, error) {
 	}, nil
 }
 
+func (p *Parser) parseTypedPlaceholder(pos Pos) (Expr, error) {
+	if _, err := p.expectTokenKind(TokenKindLBrace); err != nil {
+		return nil, err
+	}
+
+	name, err := p.parseIdent()
+	if err != nil {
+		return nil, err
+	}
+	if _, err := p.expectTokenKind(TokenKindColon); err != nil {
+		return nil, err
+	}
+	columnType, err := p.parseColumnType(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+
+	if _, err := p.expectTokenKind(TokenKindRBrace); err != nil {
+		return nil, err
+	}
+	return &TypedPlaceholder{
+		LeftBracePos:  pos,
+		RightBracePos: p.Pos(),
+		Name:          name,
+		Type:          columnType,
+	}, nil
+}
+
 func (p *Parser) parseAssignmentValues(pos Pos) (*AssignmentValues, error) {
 	if _, err := p.expectTokenKind(TokenKindLParen); err != nil {
 		return nil, err
@@ -1214,6 +1242,9 @@ func (p *Parser) parseAssignmentValues(pos Pos) (*AssignmentValues, error) {
 		switch {
 		case p.matchTokenKind(TokenKindLParen):
 			value, err = p.parseAssignmentValues(p.Pos())
+		case p.matchTokenKind(TokenKindLBrace):
+			// placeholder with type, e.g. {a :Int32}, {b :DateTime(6)}
+			value, err = p.parseTypedPlaceholder(p.Pos())
 		default:
 			value, err = p.parseExpr(p.Pos())
 		}


Test patch:
diff --git a/parser/testdata/dml/format/insert_with_placeholder.sql b/parser/testdata/dml/format/insert_with_placeholder.sql
index b6e51dc..dbf23ee 100644
--- a/parser/testdata/dml/format/insert_with_placeholder.sql
+++ b/parser/testdata/dml/format/insert_with_placeholder.sql
@@ -4,6 +4,11 @@ INSERT INTO t0(user_id, message, timestamp, metric) VALUES
     (?, ?, ?, ?),
     (?, ?, ?, ?),
     (?, ?, ?, ?)
+;
+
+INSERT INTO test_with_typed_columns (id, created_at)
+VALUES ({id: Int32}, {created_at: DateTime64(6)});
 
 -- Format SQL:
 INSERT INTO TABLE t0 (user_id, message, timestamp, metric) VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?);
+INSERT INTO TABLE test_with_typed_columns (id, created_at) VALUES ({id:Int32}, {created_at:DateTime64(6)});
diff --git a/parser/testdata/dml/insert_with_placeholder.sql b/parser/testdata/dml/insert_with_placeholder.sql
index a698479..3e52f8e 100644
--- a/parser/testdata/dml/insert_with_placeholder.sql
+++ b/parser/testdata/dml/insert_with_placeholder.sql
@@ -2,4 +2,8 @@ INSERT INTO t0(user_id, message, timestamp, metric) VALUES
     (?, ?, ?, ?),
     (?, ?, ?, ?),
     (?, ?, ?, ?),
-    (?, ?, ?, ?)
\ No newline at end of file
+    (?, ?, ?, ?)
+;
+
+INSERT INTO test_with_typed_columns (id, created_at)
+VALUES ({id: Int32}, {created_at: DateTime64(6)});
\ No newline at end of file
diff --git a/parser/testdata/dml/output/insert_with_placeholder.sql.golden.json b/parser/testdata/dml/output/insert_with_placeholder.sql.golden.json
index df1099a..64249a9 100644
--- a/parser/testdata/dml/output/insert_with_placeholder.sql.golden.json
+++ b/parser/testdata/dml/output/insert_with_placeholder.sql.golden.json
@@ -160,5 +160,97 @@
       }
     ],
     "SelectExpr": null
+  },
+  {
+    "InsertPos": 133,
+    "Format": null,
+    "Table": {
+      "Database": null,
+      "Table": {
+        "Name": "test_with_typed_columns",
+        "QuoteType": 1,
+        "NamePos": 145,
+        "NameEnd": 168
+      }
+    },
+    "ColumnNames": {
+      "LeftParenPos": 169,
+      "RightParenPos": 184,
+      "ColumnNames": [
+        {
+          "Ident": {
+            "Name": "id",
+            "QuoteType": 1,
+            "NamePos": 170,
+            "NameEnd": 172
+          },
+          "DotIdent": null
+        },
+        {
+          "Ident": {
+            "Name": "created_at",
+            "QuoteType": 1,
+            "NamePos": 174,
+            "NameEnd": 184
+          },
+          "DotIdent": null
+        }
+      ]
+    },
+    "Values": [
+      {
+        "LeftParenPos": 193,
+        "RightParenPos": 234,
+        "Values": [
+          {
+            "LeftBracePos": 194,
+            "RightBracePos": 205,
+            "Name": {
+              "Name": "id",
+              "QuoteType": 1,
+              "NamePos": 195,
+              "NameEnd": 197
+            },
+            "Type": {
+              "Name": {
+                "Name": "Int32",
+                "QuoteType": 1,
+                "NamePos": 199,
+                "NameEnd": 204
+              }
+            }
+          },
+          {
+            "LeftBracePos": 207,
+            "RightBracePos": 234,
+            "Name": {
+              "Name": "created_at",
+              "QuoteType": 1,
+              "NamePos": 208,
+              "NameEnd": 218
+            },
+            "Type": {
+              "LeftParenPos": 231,
+              "RightParenPos": 232,
+              "Name": {
+                "Name": "DateTime64",
+                "QuoteType": 1,
+                "NamePos": 220,
+                "NameEnd": 230
+              },
+              "Params": [
+                {
+                  "NumPos": 231,
+                  "NumEnd": 232,
+                  "Literal": "6",
+                  "Base": 10
+                }
+              ]
+            }
+          }
+        ]
+      }
+    ],
+    "SelectExpr": null
   }
 ]
\ No newline at end of file