Repository: AfterShip/clickhouse-sql-parser
Pull request #173 (2025-07-10 18:58:25)

fix: change WINDOW clause position
WINDOW clause should come after HAVING clause

git-hulk: @Blackmorse Many thanks to your fix.

Patch:
diff --git a/parser/ast.go b/parser/ast.go
index 096c6c6..90fdd25 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -5849,7 +5849,7 @@ func (w *WindowClause) String() string {
 	var builder strings.Builder
 	builder.WriteString("WINDOW ")
 	builder.WriteString(w.Name.String())
-	builder.WriteString(" ")
+	builder.WriteString(" AS ")
 	builder.WriteString(w.WindowExpr.String())
 	return builder.String()
 }
diff --git a/parser/parser_query.go b/parser/parser_query.go
index e14302c..b456d08 100644
--- a/parser/parser_query.go
+++ b/parser/parser_query.go
@@ -864,13 +864,6 @@ func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: fun
 	if arrayJoin != nil {
 		statementEnd = arrayJoin.End()
 	}
-	window, err := p.tryParseWindowClause(p.Pos())
-	if err != nil {
-		return nil, err
-	}
-	if window != nil {
-		statementEnd = window.End()
-	}
 	prewhere, err := p.tryParsePrewhereClause(p.Pos())
 	if err != nil {
 		return nil, err
@@ -892,7 +885,6 @@ func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: fun
 	if groupBy != nil {
 		statementEnd = groupBy.End()
 	}
-
 	withTotal := false
 	lastPos := p.Pos()
 	if p.tryConsumeKeywords(KeywordWith) {
@@ -909,6 +901,13 @@ func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: fun
 	if having != nil {
 		statementEnd = having.End()
 	}
+	window, err := p.tryParseWindowClause(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+	if window != nil {
+		statementEnd = window.End()
+	}
 	orderBy, err := p.tryParseOrderByClause(p.Pos())
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/parser/testdata/query/format/select_with_window_function.sql b/parser/testdata/query/format/select_with_window_function.sql
index c0d4b72..cba6b84 100644
--- a/parser/testdata/query/format/select_with_window_function.sql
+++ b/parser/testdata/query/format/select_with_window_function.sql
@@ -7,8 +7,11 @@ SELECT aggregation_target AS aggregation_target,
     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
+    ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2,
+    min(latest_1) OVER w AS latest_1
 FROM t0
+WINDOW w AS (PARTITION BY aggregation_target
+    ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING);
 
 -- 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;
+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, min(latest_1) OVER w AS latest_1 FROM t0 WINDOW w AS ( PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS  BETWEEN  UNBOUNDED PRECEDING AND  0 PRECEDING);
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
index e418104..7a3d481 100644
--- a/parser/testdata/query/output/select_with_window_function.sql.golden.json
+++ b/parser/testdata/query/output/select_with_window_function.sql.golden.json
@@ -1,7 +1,7 @@
 [
   {
     "SelectPos": 0,
-    "StatementEnd": 348,
+    "StatementEnd": 508,
     "With": null,
     "Top": null,
     "HasDistinct": false,
@@ -222,33 +222,158 @@
           "NamePos": 332,
           "NameEnd": 340
         }
+      },
+      {
+        "Expr": {
+          "Function": {
+            "Name": {
+              "Name": "min",
+              "QuoteType": 1,
+              "NamePos": 346,
+              "NameEnd": 349
+            },
+            "Params": {
+              "LeftParenPos": 349,
+              "RightParenPos": 358,
+              "Items": {
+                "ListPos": 350,
+                "ListEnd": 358,
+                "HasDistinct": false,
+                "Items": [
+                  {
+                    "Expr": {
+                      "Name": "latest_1",
+                      "QuoteType": 1,
+                      "NamePos": 350,
+                      "NameEnd": 358
+                    },
+                    "Alias": null
+                  }
+                ]
+              },
+              "ColumnArgList": null
+            }
+          },
+          "OverPos": 360,
+          "OverExpr": {
+            "Name": "w",
+            "QuoteType": 1,
+            "NamePos": 365,
+            "NameEnd": 366
+          }
+        },
+        "Modifiers": [],
+        "Alias": {
+          "Name": "latest_1",
+          "QuoteType": 1,
+          "NamePos": 370,
+          "NameEnd": 378
+        }
       }
     ],
     "From": {
-      "FromPos": 341,
+      "FromPos": 379,
       "Expr": {
         "Table": {
-          "TablePos": 346,
-          "TableEnd": 348,
+          "TablePos": 384,
+          "TableEnd": 386,
           "Alias": null,
           "Expr": {
             "Database": null,
             "Table": {
               "Name": "t0",
               "QuoteType": 1,
-              "NamePos": 346,
-              "NameEnd": 348
+              "NamePos": 384,
+              "NameEnd": 386
             }
           },
           "HasFinal": false
         },
-        "StatementEnd": 348,
+        "StatementEnd": 386,
         "SampleRatio": null,
         "HasFinal": false
       }
     },
     "ArrayJoin": null,
-    "Window": null,
+    "Window": {
+      "LeftParenPos": 399,
+      "RightParenPos": 508,
+      "PartitionBy": {
+        "PartitionPos": 399,
+        "Expr": {
+          "ListPos": 413,
+          "ListEnd": 431,
+          "HasDistinct": false,
+          "Items": [
+            {
+              "Expr": {
+                "Name": "aggregation_target",
+                "QuoteType": 1,
+                "NamePos": 413,
+                "NameEnd": 431
+              },
+              "Alias": null
+            }
+          ]
+        }
+      },
+      "OrderBy": {
+        "OrderPos": 436,
+        "ListEnd": 454,
+        "Items": [
+          {
+            "OrderPos": 436,
+            "Expr": {
+              "Name": "timestamp",
+              "QuoteType": 1,
+              "NamePos": 445,
+              "NameEnd": 454
+            },
+            "Alias": null,
+            "Direction": "DESC"
+          }
+        ]
+      },
+      "Frame": {
+        "FramePos": 460,
+        "Type": "ROWS",
+        "Extend": {
+          "Expr": null,
+          "Between": {
+            "FramePos": 473,
+            "Type": "",
+            "Extend": {
+              "UnboundedPos": 473,
+              "UnboundedEnd": 0,
+              "Direction": "PRECEDING"
+            }
+          },
+          "AndPos": 493,
+          "And": {
+            "FramePos": 497,
+            "Type": "",
+            "Extend": {
+              "Number": {
+                "NumPos": 497,
+                "NumEnd": 498,
+                "Literal": "0",
+                "Base": 10
+              },
+              "UnboundedEnd": 508,
+              "Direction": "PRECEDING"
+            }
+          }
+        }
+      },
+      "WindowPos": 387,
+      "Name": {
+        "Name": "w",
+        "QuoteType": 1,
+        "NamePos": 394,
+        "NameEnd": 395
+      },
+      "AsPos": 0
+    },
     "Prewhere": null,
     "Where": null,
     "GroupBy": null,
diff --git a/parser/testdata/query/select_with_window_function.sql b/parser/testdata/query/select_with_window_function.sql
index 8e52b5b..eab2136 100644
--- a/parser/testdata/query/select_with_window_function.sql
+++ b/parser/testdata/query/select_with_window_function.sql
@@ -6,5 +6,8 @@ SELECT aggregation_target AS aggregation_target,
     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
+    ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2,
+    min(latest_1) OVER w AS latest_1
+FROM t0
+WINDOW w AS (PARTITION BY aggregation_target
+    ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING);
\ No newline at end of file