Repository: AfterShip/clickhouse-sql-parser
Pull request #168 (2025-06-11 08:44:33)

feat: support for ALTER TABLE MODIFY QUERY
Support for https://clickhouse.com/docs/sql-reference/statements/alter/view


Patch:
diff --git a/parser/ast.go b/parser/ast.go
index 01960b4..be0b2ea 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -1200,6 +1200,40 @@ func (a *AlterTableRenameColumn) Accept(visitor ASTVisitor) error {
 	return visitor.VisitAlterTableRenameColumn(a)
 }
 
+type AlterTableModifyQuery struct {
+	ModifyPos    Pos
+	StatementEnd Pos
+	SelectExpr   *SelectQuery
+}
+
+func (a *AlterTableModifyQuery) Pos() Pos {
+	return a.ModifyPos
+}
+
+func (a *AlterTableModifyQuery) End() Pos {
+	return a.StatementEnd
+}
+
+func (a *AlterTableModifyQuery) AlterType() string {
+	return "MODIFY_QUERY"
+}
+
+func (a *AlterTableModifyQuery) String() string {
+	var builder strings.Builder
+	builder.WriteString("MODIFY QUERY ")
+	builder.WriteString(a.SelectExpr.String())
+	return builder.String()
+}
+
+func (a *AlterTableModifyQuery) Accept(visitor ASTVisitor) error {
+	visitor.enter(a)
+	defer visitor.leave(a)
+	if err := a.SelectExpr.Accept(visitor); err != nil {
+		return err
+	}
+	return visitor.VisitAlterTableModifyQuery(a)
+}
+
 type AlterTableModifyTTL struct {
 	ModifyPos    Pos
 	StatementEnd Pos
diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go
index 4969cdc..b1893df 100644
--- a/parser/ast_visitor.go
+++ b/parser/ast_visitor.go
@@ -27,6 +27,7 @@ type ASTVisitor interface {
 	VisitAlterTableMaterializeProjection(expr *AlterTableMaterializeProjection) error
 	VisitAlterTableRenameColumn(expr *AlterTableRenameColumn) error
 	VisitAlterTableModifyTTL(expr *AlterTableModifyTTL) error
+	VisitAlterTableModifyQuery(expr *AlterTableModifyQuery) error
 	VisitAlterTableModifyColumn(expr *AlterTableModifyColumn) error
 	VisitAlterTableReplacePartition(expr *AlterTableReplacePartition) error
 	VisitRemovePropertyType(expr *RemovePropertyType) error
@@ -356,6 +357,13 @@ func (v *DefaultASTVisitor) VisitAlterTableRenameColumn(expr *AlterTableRenameCo
 	return nil
 }
 
+func (v *DefaultASTVisitor) VisitAlterTableModifyQuery(expr *AlterTableModifyQuery) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
 func (v *DefaultASTVisitor) VisitAlterTableModifyTTL(expr *AlterTableModifyTTL) error {
 	if v.Visit != nil {
 		return v.Visit(expr)
diff --git a/parser/parser_alter.go b/parser/parser_alter.go
index 263f2ee..8def5f1 100644
--- a/parser/parser_alter.go
+++ b/parser/parser_alter.go
@@ -634,8 +634,16 @@ func (p *Parser) parseAlterTableModify(pos Pos) (AlterTableClause, error) {
 			StatementEnd: ttlExpr.End(),
 			TTL:          ttlExpr,
 		}, nil
+	case p.matchKeyword(KeywordQuery):
+		_ = p.lexer.consumeToken()
+		selectQuery, _ := p.parseSelectQuery(pos)
+		return &AlterTableModifyQuery{
+			ModifyPos:    pos,
+			StatementEnd: selectQuery.End(),
+			SelectExpr:   selectQuery,
+		}, nil
 	default:
-		return nil, fmt.Errorf("expected keyword: COLUMN, but got %q",
+		return nil, fmt.Errorf("expected keyword: COLUMN|TTL|QUERY, but got %q",
 			p.last().String)
 	}
 


Test patch:
diff --git a/parser/testdata/dml/alter_table_modify_query.sql b/parser/testdata/dml/alter_table_modify_query.sql
new file mode 100644
index 0000000..5677626
--- /dev/null
+++ b/parser/testdata/dml/alter_table_modify_query.sql
@@ -0,0 +1,1 @@
+ALTER TABLE test.some_mv ON CLUSTER cluster MODIFY QUERY SELECT field1, field2 FROM test.some_table WHERE count >= 3;
\ No newline at end of file
diff --git a/parser/testdata/dml/format/alter_table_modify_query.sql b/parser/testdata/dml/format/alter_table_modify_query.sql
new file mode 100644
index 0000000..6dd440c
--- /dev/null
+++ b/parser/testdata/dml/format/alter_table_modify_query.sql
@@ -0,0 +1,5 @@
+-- Origin SQL:
+ALTER TABLE test.some_mv ON CLUSTER cluster MODIFY QUERY SELECT field1, field2 FROM test.some_table WHERE count >= 3;
+
+-- Format SQL:
+ALTER TABLE test.some_mv ON CLUSTER cluster MODIFY QUERY SELECT field1, field2 FROM test.some_table WHERE count >= 3;
diff --git a/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json b/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json
new file mode 100644
index 0000000..d4b1f95
--- /dev/null
+++ b/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json
@@ -0,0 +1,126 @@
+[
+  {
+    "AlterPos": 0,
+    "StatementEnd": 116,
+    "TableIdentifier": {
+      "Database": {
+        "Name": "test",
+        "QuoteType": 1,
+        "NamePos": 12,
+        "NameEnd": 16
+      },
+      "Table": {
+        "Name": "some_mv",
+        "QuoteType": 1,
+        "NamePos": 17,
+        "NameEnd": 24
+      }
+    },
+    "OnCluster": {
+      "OnPos": 25,
+      "Expr": {
+        "Name": "cluster",
+        "QuoteType": 1,
+        "NamePos": 36,
+        "NameEnd": 43
+      }
+    },
+    "AlterExprs": [
+      {
+        "ModifyPos": 44,
+        "StatementEnd": 116,
+        "SelectExpr": {
+          "SelectPos": 57,
+          "StatementEnd": 116,
+          "With": null,
+          "Top": null,
+          "HasDistinct": false,
+          "SelectItems": [
+            {
+              "Expr": {
+                "Name": "field1",
+                "QuoteType": 1,
+                "NamePos": 64,
+                "NameEnd": 70
+              },
+              "Modifiers": [],
+              "Alias": null
+            },
+            {
+              "Expr": {
+                "Name": "field2",
+                "QuoteType": 1,
+                "NamePos": 72,
+                "NameEnd": 78
+              },
+              "Modifiers": [],
+              "Alias": null
+            }
+          ],
+          "From": {
+            "FromPos": 79,
+            "Expr": {
+              "Table": {
+                "TablePos": 84,
+                "TableEnd": 99,
+                "Alias": null,
+                "Expr": {
+                  "Database": {
+                    "Name": "test",
+                    "QuoteType": 1,
+                    "NamePos": 84,
+                    "NameEnd": 88
+                  },
+                  "Table": {
+                    "Name": "some_table",
+                    "QuoteType": 1,
+                    "NamePos": 89,
+                    "NameEnd": 99
+                  }
+                },
+                "HasFinal": false
+              },
+              "StatementEnd": 99,
+              "SampleRatio": null,
+              "HasFinal": false
+            }
+          },
+          "ArrayJoin": null,
+          "Window": null,
+          "Prewhere": null,
+          "Where": {
+            "WherePos": 100,
+            "Expr": {
+              "LeftExpr": {
+                "Name": "count",
+                "QuoteType": 1,
+                "NamePos": 106,
+                "NameEnd": 111
+              },
+              "Operation": "\u003e=",
+              "RightExpr": {
+                "NumPos": 115,
+                "NumEnd": 116,
+                "Literal": "3",
+                "Base": 10
+              },
+              "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