From 72aae9493ec6490db2b3bee58faf68ce7047a5ff Mon Sep 17 00:00:00 2001 From: ajruckman Date: Sun, 26 Oct 2025 18:12:35 -0400 Subject: [PATCH] parser: support ALTER TABLE DELETE/UPDATE mutations --- parser/ast.go | 115 ++++++++++++++++++ parser/ast_visitor.go | 24 ++++ parser/parser_alter.go | 91 +++++++++++++- parser/testdata/ddl/alter_table_delete.sql | 1 + .../ddl/alter_table_delete_with_cluster.sql | 1 + parser/testdata/ddl/alter_table_update.sql | 1 + .../ddl/alter_table_update_with_cluster.sql | 1 + .../ddl/format/alter_table_delete.sql | 6 + .../alter_table_delete_with_cluster.sql | 6 + .../ddl/format/alter_table_update.sql | 6 + .../alter_table_update_with_cluster.sql | 6 + .../output/alter_table_delete.sql.golden.json | 43 +++++++ ..._table_delete_with_cluster.sql.golden.json | 72 +++++++++++ .../output/alter_table_update.sql.golden.json | 93 ++++++++++++++ ..._table_update_with_cluster.sql.golden.json | 83 +++++++++++++ parser/walk.go | 20 +++ 16 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 parser/testdata/ddl/alter_table_delete.sql create mode 100644 parser/testdata/ddl/alter_table_delete_with_cluster.sql create mode 100644 parser/testdata/ddl/alter_table_update.sql create mode 100644 parser/testdata/ddl/alter_table_update_with_cluster.sql create mode 100644 parser/testdata/ddl/format/alter_table_delete.sql create mode 100644 parser/testdata/ddl/format/alter_table_delete_with_cluster.sql create mode 100644 parser/testdata/ddl/format/alter_table_update.sql create mode 100644 parser/testdata/ddl/format/alter_table_update_with_cluster.sql create mode 100644 parser/testdata/ddl/output/alter_table_delete.sql.golden.json create mode 100644 parser/testdata/ddl/output/alter_table_delete_with_cluster.sql.golden.json create mode 100644 parser/testdata/ddl/output/alter_table_update.sql.golden.json create mode 100644 parser/testdata/ddl/output/alter_table_update_with_cluster.sql.golden.json diff --git a/parser/ast.go b/parser/ast.go index bb660fe..ab170fe 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -1442,6 +1442,121 @@ func (a *AlterTableReplacePartition) Accept(visitor ASTVisitor) error { return visitor.VisitAlterTableReplacePartition(a) } +type AlterTableDelete struct { + DeletePos Pos + StatementEnd Pos + WhereClause Expr +} + +func (a *AlterTableDelete) Pos() Pos { + return a.DeletePos +} + +func (a *AlterTableDelete) End() Pos { + return a.StatementEnd +} + +func (a *AlterTableDelete) AlterType() string { + return "DELETE" +} + +func (a *AlterTableDelete) String() string { + var builder strings.Builder + builder.WriteString("DELETE WHERE ") + builder.WriteString(a.WhereClause.String()) + return builder.String() +} + +func (a *AlterTableDelete) Accept(visitor ASTVisitor) error { + visitor.Enter(a) + defer visitor.Leave(a) + if err := a.WhereClause.Accept(visitor); err != nil { + return err + } + return visitor.VisitAlterTableDelete(a) +} + +type AlterTableUpdate struct { + UpdatePos Pos + StatementEnd Pos + Assignments []*UpdateAssignment + WhereClause Expr +} + +func (a *AlterTableUpdate) Pos() Pos { + return a.UpdatePos +} + +func (a *AlterTableUpdate) End() Pos { + return a.StatementEnd +} + +func (a *AlterTableUpdate) AlterType() string { + return "UPDATE" +} + +func (a *AlterTableUpdate) String() string { + var builder strings.Builder + builder.WriteString("UPDATE ") + for i, assignment := range a.Assignments { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString(assignment.String()) + } + builder.WriteString(" WHERE ") + builder.WriteString(a.WhereClause.String()) + return builder.String() +} + +func (a *AlterTableUpdate) Accept(visitor ASTVisitor) error { + visitor.Enter(a) + defer visitor.Leave(a) + for _, assignment := range a.Assignments { + if err := assignment.Accept(visitor); err != nil { + return err + } + } + if err := a.WhereClause.Accept(visitor); err != nil { + return err + } + return visitor.VisitAlterTableUpdate(a) +} + +type UpdateAssignment struct { + AssignmentPos Pos + Column *NestedIdentifier + Expr Expr +} + +func (u *UpdateAssignment) Pos() Pos { + return u.AssignmentPos +} + +func (u *UpdateAssignment) End() Pos { + return u.Expr.End() +} + +func (u *UpdateAssignment) String() string { + var builder strings.Builder + builder.WriteString(u.Column.String()) + builder.WriteString(" = ") + builder.WriteString(u.Expr.String()) + return builder.String() +} + +func (u *UpdateAssignment) Accept(visitor ASTVisitor) error { + visitor.Enter(u) + defer visitor.Leave(u) + if err := u.Column.Accept(visitor); err != nil { + return err + } + if err := u.Expr.Accept(visitor); err != nil { + return err + } + return visitor.VisitUpdateAssignment(u) +} + type RemovePropertyType struct { RemovePos Pos diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go index 2521949..269e264 100644 --- a/parser/ast_visitor.go +++ b/parser/ast_visitor.go @@ -32,6 +32,9 @@ type ASTVisitor interface { VisitAlterTableModifySetting(expr *AlterTableModifySetting) error VisitAlterTableResetSetting(expr *AlterTableResetSetting) error VisitAlterTableReplacePartition(expr *AlterTableReplacePartition) error + VisitAlterTableDelete(expr *AlterTableDelete) error + VisitAlterTableUpdate(expr *AlterTableUpdate) error + VisitUpdateAssignment(expr *UpdateAssignment) error VisitRemovePropertyType(expr *RemovePropertyType) error VisitTableIndex(expr *TableIndex) error VisitIdent(expr *Ident) error @@ -420,6 +423,27 @@ func (v *DefaultASTVisitor) VisitAlterTableReplacePartition(expr *AlterTableRepl return nil } +func (v *DefaultASTVisitor) VisitAlterTableDelete(expr *AlterTableDelete) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitAlterTableUpdate(expr *AlterTableUpdate) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitUpdateAssignment(expr *UpdateAssignment) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + func (v *DefaultASTVisitor) VisitRemovePropertyType(expr *RemovePropertyType) error { if v.Visit != nil { return v.Visit(expr) diff --git a/parser/parser_alter.go b/parser/parser_alter.go index 8b1c60c..da9bd3e 100644 --- a/parser/parser_alter.go +++ b/parser/parser_alter.go @@ -53,8 +53,12 @@ func (p *Parser) parseAlterTable(pos Pos) (*AlterTable, error) { alter, err = p.parseAlterTableMaterialize(p.Pos()) case p.matchKeyword(KeywordReset): alter, err = p.parseAlterTableReset(p.Pos()) + case p.matchKeyword(KeywordDelete): + alter, err = p.parseAlterTableDelete(p.Pos()) + case p.matchKeyword(KeywordUpdate): + alter, err = p.parseAlterTableUpdate(p.Pos()) default: - return nil, errors.New("expected token: ADD|DROP|ATTACH|DETACH|FREEZE|REMOVE|CLEAR|MODIFY|REPLACE|MATERIALIZE|RESET") + return nil, errors.New("expected token: ADD|DROP|ATTACH|DETACH|FREEZE|REMOVE|CLEAR|MODIFY|REPLACE|MATERIALIZE|RESET|DELETE|UPDATE") } if err != nil { return nil, err @@ -833,3 +837,88 @@ func (p *Parser) parseAlterTableReset(pos Pos) (AlterTableClause, error) { Settings: settings, }, nil } + +// Syntax: ALTER TABLE DELETE WHERE condition +func (p *Parser) parseAlterTableDelete(pos Pos) (AlterTableClause, error) { + if err := p.expectKeyword(KeywordDelete); err != nil { + return nil, err + } + + if err := p.expectKeyword(KeywordWhere); err != nil { + return nil, err + } + + whereExpr, err := p.parseExpr(p.Pos()) + if err != nil { + return nil, err + } + + return &AlterTableDelete{ + DeletePos: pos, + StatementEnd: whereExpr.End(), + WhereClause: whereExpr, + }, nil +} + +// Syntax: ALTER TABLE UPDATE column1 = expr1 [, column2 = expr2, ...] WHERE condition +func (p *Parser) parseAlterTableUpdate(pos Pos) (AlterTableClause, error) { + if err := p.expectKeyword(KeywordUpdate); err != nil { + return nil, err + } + + // Parse at least one assignment + assignments := make([]*UpdateAssignment, 0) + assignment, err := p.parseUpdateAssignment(p.Pos()) + if err != nil { + return nil, err + } + assignments = append(assignments, assignment) + + // Parse additional comma-separated assignments + for p.tryConsumeTokenKind(TokenKindComma) != nil { + assignment, err = p.parseUpdateAssignment(p.Pos()) + if err != nil { + return nil, err + } + assignments = append(assignments, assignment) + } + + if err := p.expectKeyword(KeywordWhere); err != nil { + return nil, err + } + + whereExpr, err := p.parseExpr(p.Pos()) + if err != nil { + return nil, err + } + + return &AlterTableUpdate{ + UpdatePos: pos, + StatementEnd: whereExpr.End(), + Assignments: assignments, + WhereClause: whereExpr, + }, nil +} + +// Parse column = expression assignment +func (p *Parser) parseUpdateAssignment(pos Pos) (*UpdateAssignment, error) { + column, err := p.ParseNestedIdentifier(p.Pos()) + if err != nil { + return nil, err + } + + if err := p.expectTokenKind(TokenKindSingleEQ); err != nil { + return nil, err + } + + expr, err := p.parseExpr(p.Pos()) + if err != nil { + return nil, err + } + + return &UpdateAssignment{ + AssignmentPos: pos, + Column: column, + Expr: expr, + }, nil +} diff --git a/parser/testdata/ddl/alter_table_delete.sql b/parser/testdata/ddl/alter_table_delete.sql new file mode 100644 index 0000000..1e6bc8a --- /dev/null +++ b/parser/testdata/ddl/alter_table_delete.sql @@ -0,0 +1 @@ +ALTER TABLE test.events DELETE WHERE created_at < '2023-01-01'; diff --git a/parser/testdata/ddl/alter_table_delete_with_cluster.sql b/parser/testdata/ddl/alter_table_delete_with_cluster.sql new file mode 100644 index 0000000..1a06a99 --- /dev/null +++ b/parser/testdata/ddl/alter_table_delete_with_cluster.sql @@ -0,0 +1 @@ +ALTER TABLE test.events ON CLUSTER 'default_cluster' DELETE WHERE id = 123 AND status = 'deleted'; diff --git a/parser/testdata/ddl/alter_table_update.sql b/parser/testdata/ddl/alter_table_update.sql new file mode 100644 index 0000000..aa6c56c --- /dev/null +++ b/parser/testdata/ddl/alter_table_update.sql @@ -0,0 +1 @@ +ALTER TABLE test.users UPDATE status = 'active', updated_at = now() WHERE status = 'pending'; diff --git a/parser/testdata/ddl/alter_table_update_with_cluster.sql b/parser/testdata/ddl/alter_table_update_with_cluster.sql new file mode 100644 index 0000000..8b38c6a --- /dev/null +++ b/parser/testdata/ddl/alter_table_update_with_cluster.sql @@ -0,0 +1 @@ +ALTER TABLE db.table ON CLUSTER cluster1 UPDATE column1 = column1 + 1 WHERE id > 100; diff --git a/parser/testdata/ddl/format/alter_table_delete.sql b/parser/testdata/ddl/format/alter_table_delete.sql new file mode 100644 index 0000000..0a63787 --- /dev/null +++ b/parser/testdata/ddl/format/alter_table_delete.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +ALTER TABLE test.events DELETE WHERE created_at < '2023-01-01'; + + +-- Format SQL: +ALTER TABLE test.events DELETE WHERE created_at < '2023-01-01'; diff --git a/parser/testdata/ddl/format/alter_table_delete_with_cluster.sql b/parser/testdata/ddl/format/alter_table_delete_with_cluster.sql new file mode 100644 index 0000000..b62ad05 --- /dev/null +++ b/parser/testdata/ddl/format/alter_table_delete_with_cluster.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +ALTER TABLE test.events ON CLUSTER 'default_cluster' DELETE WHERE id = 123 AND status = 'deleted'; + + +-- Format SQL: +ALTER TABLE test.events ON CLUSTER 'default_cluster' DELETE WHERE id = 123 AND status = 'deleted'; diff --git a/parser/testdata/ddl/format/alter_table_update.sql b/parser/testdata/ddl/format/alter_table_update.sql new file mode 100644 index 0000000..a40d1e5 --- /dev/null +++ b/parser/testdata/ddl/format/alter_table_update.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +ALTER TABLE test.users UPDATE status = 'active', updated_at = now() WHERE status = 'pending'; + + +-- Format SQL: +ALTER TABLE test.users UPDATE status = 'active', updated_at = now() WHERE status = 'pending'; diff --git a/parser/testdata/ddl/format/alter_table_update_with_cluster.sql b/parser/testdata/ddl/format/alter_table_update_with_cluster.sql new file mode 100644 index 0000000..d1d6a20 --- /dev/null +++ b/parser/testdata/ddl/format/alter_table_update_with_cluster.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +ALTER TABLE db.table ON CLUSTER cluster1 UPDATE column1 = column1 + 1 WHERE id > 100; + + +-- Format SQL: +ALTER TABLE db.table ON CLUSTER cluster1 UPDATE column1 = column1 + 1 WHERE id > 100; diff --git a/parser/testdata/ddl/output/alter_table_delete.sql.golden.json b/parser/testdata/ddl/output/alter_table_delete.sql.golden.json new file mode 100644 index 0000000..e6270b4 --- /dev/null +++ b/parser/testdata/ddl/output/alter_table_delete.sql.golden.json @@ -0,0 +1,43 @@ +[ + { + "AlterPos": 0, + "StatementEnd": 61, + "TableIdentifier": { + "Database": { + "Name": "test", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 16 + }, + "Table": { + "Name": "events", + "QuoteType": 1, + "NamePos": 17, + "NameEnd": 23 + } + }, + "OnCluster": null, + "AlterExprs": [ + { + "DeletePos": 24, + "StatementEnd": 61, + "WhereClause": { + "LeftExpr": { + "Name": "created_at", + "QuoteType": 1, + "NamePos": 37, + "NameEnd": 47 + }, + "Operation": "\u003c", + "RightExpr": { + "LiteralPos": 51, + "LiteralEnd": 61, + "Literal": "2023-01-01" + }, + "HasGlobal": false, + "HasNot": false + } + } + ] + } +] \ No newline at end of file diff --git a/parser/testdata/ddl/output/alter_table_delete_with_cluster.sql.golden.json b/parser/testdata/ddl/output/alter_table_delete_with_cluster.sql.golden.json new file mode 100644 index 0000000..158db21 --- /dev/null +++ b/parser/testdata/ddl/output/alter_table_delete_with_cluster.sql.golden.json @@ -0,0 +1,72 @@ +[ + { + "AlterPos": 0, + "StatementEnd": 96, + "TableIdentifier": { + "Database": { + "Name": "test", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 16 + }, + "Table": { + "Name": "events", + "QuoteType": 1, + "NamePos": 17, + "NameEnd": 23 + } + }, + "OnCluster": { + "OnPos": 24, + "Expr": { + "LiteralPos": 36, + "LiteralEnd": 51, + "Literal": "default_cluster" + } + }, + "AlterExprs": [ + { + "DeletePos": 53, + "StatementEnd": 96, + "WhereClause": { + "LeftExpr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 66, + "NameEnd": 68 + }, + "Operation": "=", + "RightExpr": { + "NumPos": 71, + "NumEnd": 74, + "Literal": "123", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + }, + "Operation": "AND", + "RightExpr": { + "LeftExpr": { + "Name": "status", + "QuoteType": 1, + "NamePos": 79, + "NameEnd": 85 + }, + "Operation": "=", + "RightExpr": { + "LiteralPos": 89, + "LiteralEnd": 96, + "Literal": "deleted" + }, + "HasGlobal": false, + "HasNot": false + }, + "HasGlobal": false, + "HasNot": false + } + } + ] + } +] \ No newline at end of file diff --git a/parser/testdata/ddl/output/alter_table_update.sql.golden.json b/parser/testdata/ddl/output/alter_table_update.sql.golden.json new file mode 100644 index 0000000..b64d06d --- /dev/null +++ b/parser/testdata/ddl/output/alter_table_update.sql.golden.json @@ -0,0 +1,93 @@ +[ + { + "AlterPos": 0, + "StatementEnd": 91, + "TableIdentifier": { + "Database": { + "Name": "test", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 16 + }, + "Table": { + "Name": "users", + "QuoteType": 1, + "NamePos": 17, + "NameEnd": 22 + } + }, + "OnCluster": null, + "AlterExprs": [ + { + "UpdatePos": 23, + "StatementEnd": 91, + "Assignments": [ + { + "AssignmentPos": 30, + "Column": { + "Ident": { + "Name": "status", + "QuoteType": 1, + "NamePos": 30, + "NameEnd": 36 + }, + "DotIdent": null + }, + "Expr": { + "LiteralPos": 40, + "LiteralEnd": 46, + "Literal": "active" + } + }, + { + "AssignmentPos": 49, + "Column": { + "Ident": { + "Name": "updated_at", + "QuoteType": 1, + "NamePos": 49, + "NameEnd": 59 + }, + "DotIdent": null + }, + "Expr": { + "Name": { + "Name": "now", + "QuoteType": 1, + "NamePos": 62, + "NameEnd": 65 + }, + "Params": { + "LeftParenPos": 65, + "RightParenPos": 66, + "Items": { + "ListPos": 66, + "ListEnd": 66, + "HasDistinct": false, + "Items": [] + }, + "ColumnArgList": null + } + } + } + ], + "WhereClause": { + "LeftExpr": { + "Name": "status", + "QuoteType": 1, + "NamePos": 74, + "NameEnd": 80 + }, + "Operation": "=", + "RightExpr": { + "LiteralPos": 84, + "LiteralEnd": 91, + "Literal": "pending" + }, + "HasGlobal": false, + "HasNot": false + } + } + ] + } +] \ No newline at end of file diff --git a/parser/testdata/ddl/output/alter_table_update_with_cluster.sql.golden.json b/parser/testdata/ddl/output/alter_table_update_with_cluster.sql.golden.json new file mode 100644 index 0000000..19ee7ab --- /dev/null +++ b/parser/testdata/ddl/output/alter_table_update_with_cluster.sql.golden.json @@ -0,0 +1,83 @@ +[ + { + "AlterPos": 0, + "StatementEnd": 84, + "TableIdentifier": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 14 + }, + "Table": { + "Name": "table", + "QuoteType": 1, + "NamePos": 15, + "NameEnd": 20 + } + }, + "OnCluster": { + "OnPos": 21, + "Expr": { + "Name": "cluster1", + "QuoteType": 1, + "NamePos": 32, + "NameEnd": 40 + } + }, + "AlterExprs": [ + { + "UpdatePos": 41, + "StatementEnd": 84, + "Assignments": [ + { + "AssignmentPos": 48, + "Column": { + "Ident": { + "Name": "column1", + "QuoteType": 1, + "NamePos": 48, + "NameEnd": 55 + }, + "DotIdent": null + }, + "Expr": { + "LeftExpr": { + "Name": "column1", + "QuoteType": 1, + "NamePos": 58, + "NameEnd": 65 + }, + "Operation": "+", + "RightExpr": { + "NumPos": 68, + "NumEnd": 69, + "Literal": "1", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + } + ], + "WhereClause": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 76, + "NameEnd": 78 + }, + "Operation": "\u003e", + "RightExpr": { + "NumPos": 81, + "NumEnd": 84, + "Literal": "100", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + } + ] + } +] \ No newline at end of file diff --git a/parser/walk.go b/parser/walk.go index 81536f2..d80a8d2 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -885,6 +885,26 @@ func Walk(node Expr, fn WalkFunc) bool { if !Walk(n.Table, fn) { return false } + case *AlterTableDelete: + if !Walk(n.WhereClause, fn) { + return false + } + case *AlterTableUpdate: + for _, assignment := range n.Assignments { + if !Walk(assignment, fn) { + return false + } + } + if !Walk(n.WhereClause, fn) { + return false + } + case *UpdateAssignment: + if !Walk(n.Column, fn) { + return false + } + if !Walk(n.Expr, fn) { + return false + } case *AlterRole: for _, pair := range n.RoleRenamePairs { if !Walk(pair, fn) {