Repository: AfterShip/clickhouse-sql-parser
Pull request #127 (2025-03-03 06:45:00)

Fix the expr might be empty in window frame cause

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


### Details

* **4** of **8**   **(50.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.9%**) to **61.76%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [parser/ast.go](https://coveralls.io/builds/72505668/source?filename=parser%2Fast.go#L5202) | 4 | 8 | 50.0%
<!-- | **Total:** | **4** | **8** | **50.0%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/72505668/badge)](https://coveralls.io/builds/72505668) |
| :-- | --: |
| Change from base [Build 13621746052](https://coveralls.io/builds/72502764): |  0.9% |
| Covered Lines: | 6641 |
| Relevant Lines: | 10753 |

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


Patch:
diff --git a/parser/ast.go b/parser/ast.go
index d719a53..4933ce8 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -5199,7 +5199,10 @@ type BetweenClause struct {
 }
 
 func (f *BetweenClause) Pos() Pos {
-	return f.Expr.Pos()
+	if f.Expr != nil {
+		return f.Expr.Pos()
+	}
+	return f.Between.Pos()
 }
 
 func (f *BetweenClause) End() Pos {
@@ -5208,7 +5211,9 @@ func (f *BetweenClause) End() Pos {
 
 func (f *BetweenClause) String() string {
 	var builder strings.Builder
-	builder.WriteString(f.Expr.String())
+	if f.Expr != nil {
+		builder.WriteString(f.Expr.String())
+	}
 	builder.WriteString(" BETWEEN ")
 	builder.WriteString(f.Between.String())
 	builder.WriteString(" AND ")
@@ -5266,7 +5271,7 @@ func (f *WindowFrameUnbounded) End() Pos {
 }
 
 func (f *WindowFrameUnbounded) String() string {
-	return f.Direction + " UNBOUNDED"
+	return "UNBOUNDED " + f.Direction
 }
 
 func (f *WindowFrameUnbounded) Accept(visitor ASTVisitor) error {
diff --git a/parser/parser_query.go b/parser/parser_query.go
index 5fbcbca..79b6d28 100644
--- a/parser/parser_query.go
+++ b/parser/parser_query.go
@@ -574,7 +574,6 @@ func (p *Parser) parseWindowFrameClause(pos Pos) (*WindowFrameClause, error) {
 			return nil, err
 		}
 		expr = &BetweenClause{
-			Expr:    expr,
 			Between: betweenWindowFrame,
 			AndPos:  andPos,
 			And:     andWindowFrame,


Test patch:
diff --git a/parser/testdata/query/format/select_with_window_function.sql b/parser/testdata/query/format/select_with_window_function.sql
new file mode 100644
index 0000000..c0d4b72
--- /dev/null
+++ b/parser/testdata/query/format/select_with_window_function.sql
@@ -0,0 +1,14 @@
+-- Origin SQL:
+SELECT aggregation_target AS aggregation_target,
+    timestamp AS timestamp,
+    step_0 AS step_0,
+    latest_0 AS latest_0,
+    step_1 AS step_1,
+    latest_1 AS latest_1,
+    step_2 AS step_2,
+    min(latest_2) OVER (PARTITION BY aggregation_target
+    ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2
+FROM t0
+
+-- Format SQL:
+SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER ( PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS  BETWEEN  UNBOUNDED PRECEDING AND  0 PRECEDING) AS latest_2 FROM t0;
diff --git a/parser/testdata/query/output/select_with_window_function.sql.golden.json b/parser/testdata/query/output/select_with_window_function.sql.golden.json
new file mode 100644
index 0000000..b4e6f17
--- /dev/null
+++ b/parser/testdata/query/output/select_with_window_function.sql.golden.json
@@ -0,0 +1,265 @@
+[
+  {
+    "SelectPos": 0,
+    "StatementEnd": 348,
+    "With": null,
+    "Top": null,
+    "SelectItems": [
+      {
+        "Expr": {
+          "Name": "aggregation_target",
+          "QuoteType": 1,
+          "NamePos": 7,
+          "NameEnd": 25
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "aggregation_target",
+          "QuoteType": 1,
+          "NamePos": 29,
+          "NameEnd": 47
+        }
+      },
+      {
+        "Expr": {
+          "Name": "timestamp",
+          "QuoteType": 1,
+          "NamePos": 53,
+          "NameEnd": 62
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "timestamp",
+          "QuoteType": 1,
+          "NamePos": 66,
+          "NameEnd": 75
+        }
+      },
+      {
+        "Expr": {
+          "Name": "step_0",
+          "QuoteType": 1,
+          "NamePos": 81,
+          "NameEnd": 87
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "step_0",
+          "QuoteType": 1,
+          "NamePos": 91,
+          "NameEnd": 97
+        }
+      },
+      {
+        "Expr": {
+          "Name": "latest_0",
+          "QuoteType": 1,
+          "NamePos": 103,
+          "NameEnd": 111
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "latest_0",
+          "QuoteType": 1,
+          "NamePos": 115,
+          "NameEnd": 123
+        }
+      },
+      {
+        "Expr": {
+          "Name": "step_1",
+          "QuoteType": 1,
+          "NamePos": 129,
+          "NameEnd": 135
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "step_1",
+          "QuoteType": 1,
+          "NamePos": 139,
+          "NameEnd": 145
+        }
+      },
+      {
+        "Expr": {
+          "Name": "latest_1",
+          "QuoteType": 1,
+          "NamePos": 151,
+          "NameEnd": 159
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "latest_1",
+          "QuoteType": 1,
+          "NamePos": 163,
+          "NameEnd": 171
+        }
+      },
+      {
+        "Expr": {
+          "Name": "step_2",
+          "QuoteType": 1,
+          "NamePos": 177,
+          "NameEnd": 183
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "step_2",
+          "QuoteType": 1,
+          "NamePos": 187,
+          "NameEnd": 193
+        }
+      },
+      {
+        "Expr": {
+          "Function": {
+            "Name": {
+              "Name": "min",
+              "QuoteType": 1,
+              "NamePos": 199,
+              "NameEnd": 202
+            },
+            "Params": {
+              "LeftParenPos": 202,
+              "RightParenPos": 211,
+              "Items": {
+                "ListPos": 203,
+                "ListEnd": 211,
+                "HasDistinct": false,
+                "Items": [
+                  {
+                    "Expr": {
+                      "Name": "latest_2",
+                      "QuoteType": 1,
+                      "NamePos": 203,
+                      "NameEnd": 211
+                    },
+                    "Alias": null
+                  }
+                ]
+              },
+              "ColumnArgList": null
+            }
+          },
+          "OverPos": 213,
+          "OverExpr": {
+            "LeftParenPos": 218,
+            "RightParenPos": 327,
+            "PartitionBy": {
+              "PartitionPos": 218,
+              "Expr": {
+                "ListPos": 232,
+                "ListEnd": 250,
+                "HasDistinct": false,
+                "Items": [
+                  {
+                    "Expr": {
+                      "Name": "aggregation_target",
+                      "QuoteType": 1,
+                      "NamePos": 232,
+                      "NameEnd": 250
+                    },
+                    "Alias": null
+                  }
+                ]
+              }
+            },
+            "OrderBy": {
+              "OrderPos": 255,
+              "ListEnd": 273,
+              "Items": [
+                {
+                  "OrderPos": 255,
+                  "Expr": {
+                    "Name": "timestamp",
+                    "QuoteType": 1,
+                    "NamePos": 264,
+                    "NameEnd": 273
+                  },
+                  "Alias": null,
+                  "Direction": "DESC"
+                }
+              ]
+            },
+            "Frame": {
+              "FramePos": 279,
+              "Type": "ROWS",
+              "Extend": {
+                "Expr": null,
+                "Between": {
+                  "FramePos": 292,
+                  "Type": "",
+                  "Extend": {
+                    "UnboundedPos": 292,
+                    "UnboundedEnd": 0,
+                    "Direction": "PRECEDING"
+                  }
+                },
+                "AndPos": 312,
+                "And": {
+                  "FramePos": 316,
+                  "Type": "",
+                  "Extend": {
+                    "Number": {
+                      "NumPos": 316,
+                      "NumEnd": 317,
+                      "Literal": "0",
+                      "Base": 10
+                    },
+                    "UnboundedEnd": 327,
+                    "Direction": "PRECEDING"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "latest_2",
+          "QuoteType": 1,
+          "NamePos": 332,
+          "NameEnd": 340
+        }
+      }
+    ],
+    "From": {
+      "FromPos": 341,
+      "Expr": {
+        "Table": {
+          "TablePos": 346,
+          "TableEnd": 348,
+          "Alias": null,
+          "Expr": {
+            "Database": null,
+            "Table": {
+              "Name": "t0",
+              "QuoteType": 1,
+              "NamePos": 346,
+              "NameEnd": 348
+            }
+          },
+          "HasFinal": false
+        },
+        "StatementEnd": 348,
+        "SampleRatio": null,
+        "HasFinal": false
+      }
+    },
+    "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
diff --git a/parser/testdata/query/select_with_window_function.sql b/parser/testdata/query/select_with_window_function.sql
new file mode 100644
index 0000000..8e52b5b
--- /dev/null
+++ b/parser/testdata/query/select_with_window_function.sql
@@ -0,0 +1,10 @@
+SELECT aggregation_target AS aggregation_target,
+    timestamp AS timestamp,
+    step_0 AS step_0,
+    latest_0 AS latest_0,
+    step_1 AS step_1,
+    latest_1 AS latest_1,
+    step_2 AS step_2,
+    min(latest_2) OVER (PARTITION BY aggregation_target
+    ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2
+FROM t0
\ No newline at end of file