From b22db3fb08ae3e4a21b1a2568543ba93d4887f27 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Wed, 5 Aug 2026 15:23:31 +0200 Subject: [PATCH] Add support for SYSTEM RELOAD DICTIONARY ... ON CLUSTER SYSTEM RELOAD DICTIONARY accept an ON CLUSTER clause, but the parser rejected it: SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns SYSTEM RELOAD DICTIONARIES ON CLUSTER akvorado ClickHouse accepts the clause both before and after the dictionary name and formats both back with the cluster first, so the parser now does the same. --- parser/ast.go | 6 + parser/format.go | 4 + parser/parse_system.go | 74 ++++++---- .../testdata/ddl/format/beautify/systems.sql | 12 ++ parser/testdata/ddl/format/systems.sql | 12 ++ .../ddl/output/systems.sql.golden.json | 131 ++++++++++++++++++ parser/testdata/ddl/systems.sql | 6 + parser/walk.go | 3 + 8 files changed, 224 insertions(+), 24 deletions(-) diff --git a/parser/ast.go b/parser/ast.go index 76a794c1..36be3a00 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -5903,6 +5903,7 @@ func (s *SystemFlushExpr) Accept(visitor ASTVisitor) error { type SystemReloadExpr struct { ReloadPos Pos StatementEnd Pos + OnCluster *ClusterClause Dictionary *TableIdentifier Type string } @@ -5918,6 +5919,11 @@ func (s *SystemReloadExpr) End() Pos { func (s *SystemReloadExpr) Accept(visitor ASTVisitor) error { visitor.Enter(s) defer visitor.Leave(s) + if s.OnCluster != nil { + if err := s.OnCluster.Accept(visitor); err != nil { + return err + } + } if s.Dictionary != nil { if err := s.Dictionary.Accept(visitor); err != nil { return err diff --git a/parser/format.go b/parser/format.go index e704c4a6..e221f759 100644 --- a/parser/format.go +++ b/parser/format.go @@ -2511,6 +2511,10 @@ func (s *SystemFlushExpr) FormatSQL(formatter *Formatter) { func (s *SystemReloadExpr) FormatSQL(formatter *Formatter) { formatter.WriteString("RELOAD ") formatter.WriteString(s.Type) + if s.OnCluster != nil { + formatter.WriteByte(whitespace) + formatter.WriteExpr(s.OnCluster) + } if s.Dictionary != nil { formatter.WriteByte(whitespace) formatter.WriteExpr(s.Dictionary) diff --git a/parser/parse_system.go b/parser/parse_system.go index 98cb65fa..f75f0919 100644 --- a/parser/parse_system.go +++ b/parser/parse_system.go @@ -67,39 +67,65 @@ func (p *Parser) parseSystemReloadExpr(pos Pos) (*SystemReloadExpr, error) { return nil, err } + var typ string + var statementEnd Pos + // Only RELOAD DICTIONARY takes a dictionary name. + hasDictionaryName := false switch { case p.matchKeyword(KeywordDictionaries): - curToken := p.current() + typ = KeywordDictionaries + statementEnd = p.current().End + _ = p.lexer.consumeToken() + case p.matchKeyword(KeywordDictionary): + typ = KeywordDictionary + statementEnd = p.current().End + hasDictionaryName = true _ = p.lexer.consumeToken() - return &SystemReloadExpr{ - ReloadPos: pos, - StatementEnd: curToken.End, - Type: KeywordDictionaries, - }, nil - case p.tryConsumeKeywords(KeywordDictionary): - dictionary, err := p.parseTableIdentifier(p.Pos()) - if err != nil { - return nil, err - } - return &SystemReloadExpr{ - ReloadPos: pos, - StatementEnd: dictionary.End(), - Type: KeywordDictionary, - Dictionary: dictionary, - }, nil case p.tryConsumeKeywords(KeywordEmbedded): - curToken := p.current() + typ = "EMBEDDED DICTIONARIES" + statementEnd = p.current().End if err := p.expectKeyword(KeywordDictionaries); err != nil { return nil, err } - return &SystemReloadExpr{ - ReloadPos: pos, - StatementEnd: curToken.End, - Type: "EMBEDDED DICTIONARIES", - }, nil default: - return nil, fmt.Errorf("expected DICTIONARIES|CONFIG") + return nil, fmt.Errorf("expected DICTIONARIES|DICTIONARY|EMBEDDED") } + + onCluster, err := p.tryParseClusterClause(p.Pos()) + if err != nil { + return nil, err + } + if onCluster != nil { + statementEnd = onCluster.End() + } + + var dictionary *TableIdentifier + if hasDictionaryName { + dictionary, err = p.parseTableIdentifier(p.Pos()) + if err != nil { + return nil, err + } + statementEnd = dictionary.End() + + // ClickHouse also accepts ON CLUSTER after the dictionary name. + if onCluster == nil { + onCluster, err = p.tryParseClusterClause(p.Pos()) + if err != nil { + return nil, err + } + if onCluster != nil { + statementEnd = onCluster.End() + } + } + } + + return &SystemReloadExpr{ + ReloadPos: pos, + StatementEnd: statementEnd, + OnCluster: onCluster, + Type: typ, + Dictionary: dictionary, + }, nil } func (p *Parser) parseSystemSyncExpr(pos Pos) (*SystemSyncExpr, error) { diff --git a/parser/testdata/ddl/format/beautify/systems.sql b/parser/testdata/ddl/format/beautify/systems.sql index 2d48c794..18ba0865 100644 --- a/parser/testdata/ddl/format/beautify/systems.sql +++ b/parser/testdata/ddl/format/beautify/systems.sql @@ -2,9 +2,21 @@ SYSTEM FLUSH LOGS; SYSTEM DROP UNCOMPRESSED CACHE; SYSTEM DROP FILESYSTEM CACHE; +SYSTEM RELOAD DICTIONARIES; +SYSTEM RELOAD DICTIONARIES ON CLUSTER akvorado; +SYSTEM RELOAD DICTIONARY akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD DICTIONARY akvorado.asns ON CLUSTER akvorado; +SYSTEM RELOAD EMBEDDED DICTIONARIES ON CLUSTER akvorado; -- Beautify SQL: SYSTEM FLUSH LOGS; SYSTEM DROP UNCOMPRESSED CACHE; SYSTEM DROP FILESYSTEM CACHE; +SYSTEM RELOAD DICTIONARIES; +SYSTEM RELOAD DICTIONARIES ON CLUSTER akvorado; +SYSTEM RELOAD DICTIONARY akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD EMBEDDED DICTIONARIES ON CLUSTER akvorado; diff --git a/parser/testdata/ddl/format/systems.sql b/parser/testdata/ddl/format/systems.sql index a4b7ec6f..974f5de1 100644 --- a/parser/testdata/ddl/format/systems.sql +++ b/parser/testdata/ddl/format/systems.sql @@ -2,9 +2,21 @@ SYSTEM FLUSH LOGS; SYSTEM DROP UNCOMPRESSED CACHE; SYSTEM DROP FILESYSTEM CACHE; +SYSTEM RELOAD DICTIONARIES; +SYSTEM RELOAD DICTIONARIES ON CLUSTER akvorado; +SYSTEM RELOAD DICTIONARY akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD DICTIONARY akvorado.asns ON CLUSTER akvorado; +SYSTEM RELOAD EMBEDDED DICTIONARIES ON CLUSTER akvorado; -- Format SQL: SYSTEM FLUSH LOGS; SYSTEM DROP UNCOMPRESSED CACHE; SYSTEM DROP FILESYSTEM CACHE; +SYSTEM RELOAD DICTIONARIES; +SYSTEM RELOAD DICTIONARIES ON CLUSTER akvorado; +SYSTEM RELOAD DICTIONARY akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD EMBEDDED DICTIONARIES ON CLUSTER akvorado; diff --git a/parser/testdata/ddl/output/systems.sql.golden.json b/parser/testdata/ddl/output/systems.sql.golden.json index 6b875978..0a5b7676 100644 --- a/parser/testdata/ddl/output/systems.sql.golden.json +++ b/parser/testdata/ddl/output/systems.sql.golden.json @@ -23,5 +23,136 @@ "StatementEnd": 79, "Type": "FILESYSTEM CACHE" } + }, + { + "SystemPos": 81, + "Expr": { + "ReloadPos": 88, + "StatementEnd": 107, + "OnCluster": null, + "Dictionary": null, + "Type": "DICTIONARIES" + } + }, + { + "SystemPos": 109, + "Expr": { + "ReloadPos": 116, + "StatementEnd": 155, + "OnCluster": { + "OnPos": 136, + "Expr": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 147, + "NameEnd": 155 + } + }, + "Dictionary": null, + "Type": "DICTIONARIES" + } + }, + { + "SystemPos": 157, + "Expr": { + "ReloadPos": 164, + "StatementEnd": 195, + "OnCluster": null, + "Dictionary": { + "Database": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 182, + "NameEnd": 190 + }, + "Table": { + "Name": "asns", + "QuoteType": 1, + "NamePos": 191, + "NameEnd": 195 + } + }, + "Type": "DICTIONARY" + } + }, + { + "SystemPos": 197, + "Expr": { + "ReloadPos": 204, + "StatementEnd": 255, + "OnCluster": { + "OnPos": 222, + "Expr": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 233, + "NameEnd": 241 + } + }, + "Dictionary": { + "Database": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 242, + "NameEnd": 250 + }, + "Table": { + "Name": "asns", + "QuoteType": 1, + "NamePos": 251, + "NameEnd": 255 + } + }, + "Type": "DICTIONARY" + } + }, + { + "SystemPos": 257, + "Expr": { + "ReloadPos": 264, + "StatementEnd": 315, + "OnCluster": { + "OnPos": 296, + "Expr": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 307, + "NameEnd": 315 + } + }, + "Dictionary": { + "Database": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 282, + "NameEnd": 290 + }, + "Table": { + "Name": "asns", + "QuoteType": 1, + "NamePos": 291, + "NameEnd": 295 + } + }, + "Type": "DICTIONARY" + } + }, + { + "SystemPos": 317, + "Expr": { + "ReloadPos": 324, + "StatementEnd": 372, + "OnCluster": { + "OnPos": 353, + "Expr": { + "Name": "akvorado", + "QuoteType": 1, + "NamePos": 364, + "NameEnd": 372 + } + }, + "Dictionary": null, + "Type": "EMBEDDED DICTIONARIES" + } } ] \ No newline at end of file diff --git a/parser/testdata/ddl/systems.sql b/parser/testdata/ddl/systems.sql index 9dab08e1..a633f88c 100644 --- a/parser/testdata/ddl/systems.sql +++ b/parser/testdata/ddl/systems.sql @@ -1,3 +1,9 @@ SYSTEM FLUSH LOGS; SYSTEM DROP UNCOMPRESSED CACHE; SYSTEM DROP FILESYSTEM CACHE; +SYSTEM RELOAD DICTIONARIES; +SYSTEM RELOAD DICTIONARIES ON CLUSTER akvorado; +SYSTEM RELOAD DICTIONARY akvorado.asns; +SYSTEM RELOAD DICTIONARY ON CLUSTER akvorado akvorado.asns; +SYSTEM RELOAD DICTIONARY akvorado.asns ON CLUSTER akvorado; +SYSTEM RELOAD EMBEDDED DICTIONARIES ON CLUSTER akvorado; diff --git a/parser/walk.go b/parser/walk.go index dbeb68ed..e129a35c 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -542,6 +542,9 @@ func Walk(node Expr, fn WalkFunc) bool { return false } case *SystemReloadExpr: + if !Walk(n.OnCluster, fn) { + return false + } if !Walk(n.Dictionary, fn) { return false }