Repository: AfterShip/clickhouse-sql-parser
Pull request #95 (2024-08-26 10:42:12)

Add support of parsing query placeholder `?`

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


### Details

* **22** of **27**   **(81.48%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.06%**) to **59.215%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [parser/ast_visitor.go](https://coveralls.io/builds/69425438/source?filename=parser%2Fast_visitor.go#L1148) | 3 | 5 | 60.0%
| [parser/ast.go](https://coveralls.io/builds/69425438/source?filename=parser%2Fast.go#L3404) | 11 | 14 | 78.57%
<!-- | **Total:** | **22** | **27** | **81.48%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/69425438/badge)](https://coveralls.io/builds/69425438) |
| :-- | --: |
| Change from base [Build 10522129953](https://coveralls.io/builds/69387944): |  0.06% |
| Covered Lines: | 6053 |
| Relevant Lines: | 10222 |

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


Patch:
diff --git a/parser/ast.go b/parser/ast.go
index f412744..5b9d3ef 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -3481,6 +3481,30 @@ func (s *StringLiteral) Accept(visitor ASTVisitor) error {
 	return visitor.VisitStringLiteral(s)
 }
 
+type PlaceHolder struct {
+	PlaceholderPos Pos
+	PlaceHolderEnd Pos
+	Type           string
+}
+
+func (p *PlaceHolder) Pos() Pos {
+	return p.PlaceholderPos
+}
+
+func (p *PlaceHolder) End() Pos {
+	return p.PlaceHolderEnd
+}
+
+func (p *PlaceHolder) String(int) string {
+	return p.Type
+}
+
+func (p *PlaceHolder) Accept(visitor ASTVisitor) error {
+	visitor.enter(p)
+	defer visitor.leave(p)
+	return visitor.VisitPlaceHolderExpr(p)
+}
+
 type RatioExpr struct {
 	Numerator *NumberLiteral
 	// numberLiteral (SLASH numberLiteral)?
diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go
index c22ac11..0435dcf 100644
--- a/parser/ast_visitor.go
+++ b/parser/ast_visitor.go
@@ -143,6 +143,7 @@ type ASTVisitor interface {
 	VisitSystemDropExpr(expr *SystemDropExpr) error
 	VisitTruncateTable(expr *TruncateTable) error
 	VisitSampleRatioExpr(expr *SampleClause) error
+	VisitPlaceHolderExpr(expr *PlaceHolder) error
 	VisitDeleteFromExpr(expr *DeleteClause) error
 	VisitColumnNamesExpr(expr *ColumnNamesExpr) error
 	VisitValuesExpr(expr *AssignmentValues) error
@@ -1158,6 +1159,13 @@ func (v *DefaultASTVisitor) VisitSampleRatioExpr(expr *SampleClause) error {
 	return nil
 }
 
+func (v *DefaultASTVisitor) VisitPlaceHolderExpr(expr *PlaceHolder) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
 func (v *DefaultASTVisitor) VisitDeleteFromExpr(expr *DeleteClause) error {
 	if v.Visit != nil {
 		return v.Visit(expr)
diff --git a/parser/parser_column.go b/parser/parser_column.go
index a94fd01..3ae5111 100644
--- a/parser/parser_column.go
+++ b/parser/parser_column.go
@@ -380,6 +380,14 @@ func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen
 			return p.parseQueryParam(p.Pos())
 		}
 		return p.parseMapLiteral(p.Pos())
+	case p.matchTokenKind(opTypeQuery):
+		// Placeholder `?`
+		_ = p.lexer.consumeToken()
+		return &PlaceHolder{
+			PlaceholderPos: pos,
+			PlaceHolderEnd: pos,
+			Type:           opTypeQuery,
+		}, nil
 	default:
 		return nil, fmt.Errorf("unexpected token kind: %s", p.lastTokenKind())
 	}


Test patch:
diff --git a/parser/testdata/dml/format/insert_with_placeholder.sql b/parser/testdata/dml/format/insert_with_placeholder.sql
new file mode 100644
index 0000000..cd0c86c
--- /dev/null
+++ b/parser/testdata/dml/format/insert_with_placeholder.sql
@@ -0,0 +1,15 @@
+-- Origin SQL:
+INSERT INTO t0(user_id, message, timestamp, metric) VALUES
+    (?, ?, ?, ?),
+    (?, ?, ?, ?),
+    (?, ?, ?, ?),
+    (?, ?, ?, ?)
+
+-- Format SQL:
+INSERT INTO TABLE t0
+  (user_id, message, timestamp, metric)
+VALUES 
+  (?, ?, ?, ?),
+  (?, ?, ?, ?),
+  (?, ?, ?, ?),
+  (?, ?, ?, ?);
diff --git a/parser/testdata/dml/insert_with_placeholder.sql b/parser/testdata/dml/insert_with_placeholder.sql
new file mode 100644
index 0000000..a698479
--- /dev/null
+++ b/parser/testdata/dml/insert_with_placeholder.sql
@@ -0,0 +1,5 @@
+INSERT INTO t0(user_id, message, timestamp, metric) VALUES
+    (?, ?, ?, ?),
+    (?, ?, ?, ?),
+    (?, ?, ?, ?),
+    (?, ?, ?, ?)
\ 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
new file mode 100644
index 0000000..df1099a
--- /dev/null
+++ b/parser/testdata/dml/output/insert_with_placeholder.sql.golden.json
@@ -0,0 +1,164 @@
+[
+  {
+    "InsertPos": 0,
+    "Format": null,
+    "Table": {
+      "Database": null,
+      "Table": {
+        "Name": "t0",
+        "QuoteType": 1,
+        "NamePos": 12,
+        "NameEnd": 14
+      }
+    },
+    "ColumnNames": {
+      "LeftParenPos": 14,
+      "RightParenPos": 50,
+      "ColumnNames": [
+        {
+          "Ident": {
+            "Name": "user_id",
+            "QuoteType": 1,
+            "NamePos": 15,
+            "NameEnd": 22
+          },
+          "DotIdent": null
+        },
+        {
+          "Ident": {
+            "Name": "message",
+            "QuoteType": 1,
+            "NamePos": 24,
+            "NameEnd": 31
+          },
+          "DotIdent": null
+        },
+        {
+          "Ident": {
+            "Name": "timestamp",
+            "QuoteType": 1,
+            "NamePos": 33,
+            "NameEnd": 42
+          },
+          "DotIdent": null
+        },
+        {
+          "Ident": {
+            "Name": "metric",
+            "QuoteType": 1,
+            "NamePos": 44,
+            "NameEnd": 50
+          },
+          "DotIdent": null
+        }
+      ]
+    },
+    "Values": [
+      {
+        "LeftParenPos": 63,
+        "RightParenPos": 74,
+        "Values": [
+          {
+            "PlaceholderPos": 64,
+            "PlaceHolderEnd": 64,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 67,
+            "PlaceHolderEnd": 67,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 70,
+            "PlaceHolderEnd": 70,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 73,
+            "PlaceHolderEnd": 73,
+            "Type": "?"
+          }
+        ]
+      },
+      {
+        "LeftParenPos": 81,
+        "RightParenPos": 92,
+        "Values": [
+          {
+            "PlaceholderPos": 82,
+            "PlaceHolderEnd": 82,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 85,
+            "PlaceHolderEnd": 85,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 88,
+            "PlaceHolderEnd": 88,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 91,
+            "PlaceHolderEnd": 91,
+            "Type": "?"
+          }
+        ]
+      },
+      {
+        "LeftParenPos": 99,
+        "RightParenPos": 110,
+        "Values": [
+          {
+            "PlaceholderPos": 100,
+            "PlaceHolderEnd": 100,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 103,
+            "PlaceHolderEnd": 103,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 106,
+            "PlaceHolderEnd": 106,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 109,
+            "PlaceHolderEnd": 109,
+            "Type": "?"
+          }
+        ]
+      },
+      {
+        "LeftParenPos": 117,
+        "RightParenPos": 128,
+        "Values": [
+          {
+            "PlaceholderPos": 118,
+            "PlaceHolderEnd": 118,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 121,
+            "PlaceHolderEnd": 121,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 124,
+            "PlaceHolderEnd": 124,
+            "Type": "?"
+          },
+          {
+            "PlaceholderPos": 127,
+            "PlaceHolderEnd": 127,
+            "Type": "?"
+          }
+        ]
+      }
+    ],
+    "SelectExpr": null
+  }
+]
\ No newline at end of file
diff --git a/parser/testdata/query/format/selelct_with_placeholder.sql b/parser/testdata/query/format/selelct_with_placeholder.sql
new file mode 100644
index 0000000..9df1db7
--- /dev/null
+++ b/parser/testdata/query/format/selelct_with_placeholder.sql
@@ -0,0 +1,11 @@
+-- Origin SQL:
+SELECT * FROM t0 WHERE id = ?;
+
+-- Format SQL:
+
+SELECT 
+  *
+FROM
+  t0
+WHERE
+  id = ?;
diff --git a/parser/testdata/query/output/selelct_with_placeholder.sql.golden.json b/parser/testdata/query/output/selelct_with_placeholder.sql.golden.json
new file mode 100644
index 0000000..5173b07
--- /dev/null
+++ b/parser/testdata/query/output/selelct_with_placeholder.sql.golden.json
@@ -0,0 +1,77 @@
+[
+  {
+    "SelectPos": 0,
+    "StatementEnd": 28,
+    "With": null,
+    "Top": null,
+    "SelectColumns": {
+      "ListPos": 7,
+      "ListEnd": 7,
+      "HasDistinct": false,
+      "Items": [
+        {
+          "Name": "*",
+          "QuoteType": 0,
+          "NamePos": 7,
+          "NameEnd": 7
+        }
+      ]
+    },
+    "From": {
+      "FromPos": 9,
+      "Expr": {
+        "Table": {
+          "TablePos": 14,
+          "TableEnd": 16,
+          "Alias": null,
+          "Expr": {
+            "Database": null,
+            "Table": {
+              "Name": "t0",
+              "QuoteType": 1,
+              "NamePos": 14,
+              "NameEnd": 16
+            }
+          },
+          "HasFinal": false
+        },
+        "StatementEnd": 16,
+        "SampleRatio": null,
+        "HasFinal": false
+      }
+    },
+    "ArrayJoin": null,
+    "Window": null,
+    "Prewhere": null,
+    "Where": {
+      "WherePos": 17,
+      "Expr": {
+        "LeftExpr": {
+          "Name": "id",
+          "QuoteType": 1,
+          "NamePos": 23,
+          "NameEnd": 25
+        },
+        "Operation": "=",
+        "RightExpr": {
+          "PlaceholderPos": 28,
+          "PlaceHolderEnd": 28,
+          "Type": "?"
+        },
+        "HasGlobal": false,
+        "HasNot": false
+      }
+    },
+    "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
diff --git a/parser/testdata/query/selelct_with_placeholder.sql b/parser/testdata/query/selelct_with_placeholder.sql
new file mode 100644
index 0000000..f325105
--- /dev/null
+++ b/parser/testdata/query/selelct_with_placeholder.sql
@@ -0,0 +1,1 @@
+SELECT * FROM t0 WHERE id = ?;
\ No newline at end of file