From 64cffb24cda132987cda8ea17673509ef90ea42a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Dec 2025 23:53:21 +0000 Subject: [PATCH] Add query handlers for GRANT, SHOW GRANTS, EXISTS variants, and SHOW CREATE variants This commit adds parser and EXPLAIN output support for: - GrantQuery: GRANT and REVOKE statements - ShowGrantsQuery: SHOW GRANTS statement - ExistsDatabaseQuery: EXISTS DATABASE statement - ExistsDictionaryQuery: EXISTS DICTIONARY statement - ExistsViewQuery: EXISTS VIEW statement - ShowCreateDictionaryQuery: SHOW CREATE DICTIONARY statement - ShowCreateViewQuery: SHOW CREATE VIEW statement Changes: - Add ExistsType enum to differentiate EXISTS query types - Add GrantQuery and ShowGrantsQuery AST types - Add ShowCreateDictionary and ShowCreateView ShowTypes - Update parser to recognize DICTIONARY, VIEW keywords in EXISTS statements - Update parser to handle GRANT, REVOKE, SHOW GRANTS statements - Update parser to handle SHOW CREATE DICTIONARY/VIEW statements - Update EXPLAIN handlers to output correct query type names - Remove passing tests from explain_todo in metadata files --- ast/ast.go | 56 ++++++++++--- internal/explain/explain.go | 4 + internal/explain/statements.go | 57 ++++++++++++- parser/parser.go | 79 ++++++++++++++++--- .../metadata.json | 7 -- .../testdata/01048_exists_query/metadata.json | 18 +---- .../01074_partial_revokes/metadata.json | 63 +-------------- .../metadata.json | 3 +- .../testdata/01292_create_user/metadata.json | 7 -- .../01702_system_query_log/metadata.json | 5 -- .../01999_grant_with_replace/metadata.json | 31 +------- .../metadata.json | 3 +- .../metadata.json | 1 - .../metadata.json | 11 +-- .../metadata.json | 8 +- 15 files changed, 180 insertions(+), 173 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 9565e68b69..ab3a95dc01 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -652,15 +652,17 @@ func (s *ShowQuery) statementNode() {} type ShowType string const ( - ShowTables ShowType = "TABLES" - ShowDatabases ShowType = "DATABASES" - ShowProcesses ShowType = "PROCESSLIST" - ShowCreate ShowType = "CREATE" - ShowCreateDB ShowType = "CREATE_DATABASE" - ShowColumns ShowType = "COLUMNS" - ShowDictionaries ShowType = "DICTIONARIES" - ShowFunctions ShowType = "FUNCTIONS" - ShowSettings ShowType = "SETTINGS" + ShowTables ShowType = "TABLES" + ShowDatabases ShowType = "DATABASES" + ShowProcesses ShowType = "PROCESSLIST" + ShowCreate ShowType = "CREATE" + ShowCreateDB ShowType = "CREATE_DATABASE" + ShowCreateDictionary ShowType = "CREATE_DICTIONARY" + ShowCreateView ShowType = "CREATE_VIEW" + ShowColumns ShowType = "COLUMNS" + ShowDictionaries ShowType = "DICTIONARIES" + ShowFunctions ShowType = "FUNCTIONS" + ShowSettings ShowType = "SETTINGS" ) // ExplainQuery represents an EXPLAIN statement. @@ -770,17 +772,47 @@ func (e *ExchangeQuery) Pos() token.Position { return e.Position } func (e *ExchangeQuery) End() token.Position { return e.Position } func (e *ExchangeQuery) statementNode() {} +// ExistsType represents the type of EXISTS query. +type ExistsType string + +const ( + ExistsTable ExistsType = "TABLE" + ExistsDictionary ExistsType = "DICTIONARY" + ExistsDatabase ExistsType = "DATABASE" + ExistsView ExistsType = "VIEW" +) + // ExistsQuery represents an EXISTS table_name statement (check if table exists). type ExistsQuery struct { - Position token.Position `json:"-"` - Database string `json:"database,omitempty"` - Table string `json:"table"` + Position token.Position `json:"-"` + ExistsType ExistsType `json:"exists_type,omitempty"` + Database string `json:"database,omitempty"` + Table string `json:"table"` } func (e *ExistsQuery) Pos() token.Position { return e.Position } func (e *ExistsQuery) End() token.Position { return e.Position } func (e *ExistsQuery) statementNode() {} +// GrantQuery represents a GRANT or REVOKE statement. +type GrantQuery struct { + Position token.Position `json:"-"` + IsRevoke bool `json:"is_revoke,omitempty"` +} + +func (g *GrantQuery) Pos() token.Position { return g.Position } +func (g *GrantQuery) End() token.Position { return g.Position } +func (g *GrantQuery) statementNode() {} + +// ShowGrantsQuery represents a SHOW GRANTS statement. +type ShowGrantsQuery struct { + Position token.Position `json:"-"` +} + +func (s *ShowGrantsQuery) Pos() token.Position { return s.Position } +func (s *ShowGrantsQuery) End() token.Position { return s.Position } +func (s *ShowGrantsQuery) statementNode() {} + // ShowPrivilegesQuery represents a SHOW PRIVILEGES statement. type ShowPrivilegesQuery struct { Position token.Position `json:"-"` diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 3b004e6dc1..aca386f530 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -127,6 +127,10 @@ func Node(sb *strings.Builder, node interface{}, depth int) { fmt.Fprintf(sb, "%sShowPrivilegesQuery\n", indent) case *ast.ShowCreateQuotaQuery: fmt.Fprintf(sb, "%sSHOW CREATE QUOTA query\n", indent) + case *ast.ShowGrantsQuery: + fmt.Fprintf(sb, "%sShowGrantsQuery\n", indent) + case *ast.GrantQuery: + fmt.Fprintf(sb, "%sGrantQuery\n", indent) case *ast.UseQuery: explainUseQuery(sb, n, indent) case *ast.DescribeQuery: diff --git a/internal/explain/statements.go b/internal/explain/statements.go index 1513ad42ce..186ab35aa8 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -482,6 +482,38 @@ func explainShowQuery(sb *strings.Builder, n *ast.ShowQuery, indent string) { return } + // SHOW CREATE DICTIONARY has special output format + if n.ShowType == ast.ShowCreateDictionary && (n.Database != "" || n.From != "") { + if n.Database != "" && n.From != "" { + fmt.Fprintf(sb, "%sShowCreateDictionaryQuery %s %s (children 2)\n", indent, n.Database, n.From) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + } else if n.From != "" { + fmt.Fprintf(sb, "%sShowCreateDictionaryQuery %s (children 1)\n", indent, n.From) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + } else if n.Database != "" { + fmt.Fprintf(sb, "%sShowCreateDictionaryQuery %s (children 1)\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + } + return + } + + // SHOW CREATE VIEW has special output format + if n.ShowType == ast.ShowCreateView && (n.Database != "" || n.From != "") { + if n.Database != "" && n.From != "" { + fmt.Fprintf(sb, "%sShowCreateViewQuery %s %s (children 2)\n", indent, n.Database, n.From) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + } else if n.From != "" { + fmt.Fprintf(sb, "%sShowCreateViewQuery %s (children 1)\n", indent, n.From) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + } else if n.Database != "" { + fmt.Fprintf(sb, "%sShowCreateViewQuery %s (children 1)\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + } + return + } + // SHOW CREATE TABLE has special output format with database and table identifiers if n.ShowType == ast.ShowCreate && (n.Database != "" || n.From != "") { // Format: ShowCreateTableQuery database table (children 2) @@ -550,12 +582,33 @@ func explainDescribeQuery(sb *strings.Builder, n *ast.DescribeQuery, indent stri } func explainExistsTableQuery(sb *strings.Builder, n *ast.ExistsQuery, indent string) { - // EXISTS TABLE/DATABASE/DICTIONARY query + // Determine query type name based on ExistsType + queryType := "ExistsTableQuery" + switch n.ExistsType { + case ast.ExistsDictionary: + queryType = "ExistsDictionaryQuery" + case ast.ExistsDatabase: + queryType = "ExistsDatabaseQuery" + case ast.ExistsView: + queryType = "ExistsViewQuery" + } + + // EXISTS DATABASE has only one child (the database name stored in Table) + if n.ExistsType == ast.ExistsDatabase { + name := n.Table + fmt.Fprintf(sb, "%s%s %s (children %d)\n", indent, queryType, name, 1) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) + return + } + + // For TABLE/DICTIONARY/VIEW, show database and object name name := n.Table + children := 1 if n.Database != "" { name = n.Database + " " + n.Table + children = 2 } - fmt.Fprintf(sb, "%sExistsTableQuery %s (children %d)\n", indent, name, 2) + fmt.Fprintf(sb, "%s%s %s (children %d)\n", indent, queryType, name, children) if n.Database != "" { fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) } diff --git a/parser/parser.go b/parser/parser.go index 6a6d0c89ef..0694bce76d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -155,6 +155,10 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseAttach() case token.CHECK: return p.parseCheck() + case token.GRANT: + return p.parseGrant() + case token.REVOKE: + return p.parseRevoke() default: p.errors = append(p.errors, fmt.Errorf("unexpected token %s at line %d, column %d", p.current.Token, p.current.Pos.Line, p.current.Pos.Column)) @@ -3188,6 +3192,15 @@ func (p *Parser) parseShow() ast.Statement { return &ast.ShowPrivilegesQuery{Position: pos} } + // Handle SHOW GRANTS - it has its own statement type + if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "GRANTS" { + // Skip all remaining tokens until end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + p.nextToken() + } + return &ast.ShowGrantsQuery{Position: pos} + } + show := &ast.ShowQuery{ Position: pos, } @@ -3216,6 +3229,12 @@ func (p *Parser) parseShow() ast.Statement { p.nextToken() } return &ast.ShowCreateQuotaQuery{Position: pos, Name: name} + } else if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "DICTIONARY" { + show.ShowType = ast.ShowCreateDictionary + p.nextToken() + } else if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "VIEW" { + show.ShowType = ast.ShowCreateView + p.nextToken() } else { show.ShowType = ast.ShowCreate // Handle SHOW CREATE TABLE, etc. @@ -3242,8 +3261,9 @@ func (p *Parser) parseShow() ast.Statement { } } - // Parse FROM clause (or table/database name for SHOW CREATE TABLE/DATABASE) - if p.currentIs(token.FROM) || ((show.ShowType == ast.ShowCreate || show.ShowType == ast.ShowCreateDB) && (p.currentIs(token.IDENT) || p.current.Token.IsKeyword())) { + // Parse FROM clause (or table/database name for SHOW CREATE TABLE/DATABASE/DICTIONARY/VIEW) + showCreateTypes := show.ShowType == ast.ShowCreate || show.ShowType == ast.ShowCreateDB || show.ShowType == ast.ShowCreateDictionary || show.ShowType == ast.ShowCreateView + if p.currentIs(token.FROM) || (showCreateTypes && (p.currentIs(token.IDENT) || p.current.Token.IsKeyword())) { if p.currentIs(token.FROM) { p.nextToken() } @@ -3921,25 +3941,36 @@ func (p *Parser) parseParenthesizedSelect() *ast.SelectWithUnionQuery { // parseExistsStatement handles EXISTS table_name syntax func (p *Parser) parseExistsStatement() *ast.ExistsQuery { exists := &ast.ExistsQuery{ - Position: p.current.Pos, + Position: p.current.Pos, + ExistsType: ast.ExistsTable, // default to TABLE } p.nextToken() // skip EXISTS - // Skip optional TABLE keyword + // Check for DICTIONARY, DATABASE, VIEW, or TABLE keyword if p.currentIs(token.TABLE) { + exists.ExistsType = ast.ExistsTable + p.nextToken() + } else if p.currentIs(token.DATABASE) { + exists.ExistsType = ast.ExistsDatabase + p.nextToken() + } else if p.currentIs(token.VIEW) { + exists.ExistsType = ast.ExistsView + p.nextToken() + } else if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "DICTIONARY" { + exists.ExistsType = ast.ExistsDictionary p.nextToken() } - // Parse table name (database.table or just table) - tableName := p.parseIdentifierName() - if tableName != "" { + // Parse table/database/dictionary/view name + name := p.parseIdentifierName() + if name != "" { if p.currentIs(token.DOT) { p.nextToken() - exists.Database = tableName + exists.Database = name exists.Table = p.parseIdentifierName() } else { - exists.Table = tableName + exists.Table = name } } @@ -4034,3 +4065,33 @@ func (p *Parser) parseProjection() *ast.Projection { return proj } + +// parseGrant handles GRANT statements +func (p *Parser) parseGrant() *ast.GrantQuery { + grant := &ast.GrantQuery{ + Position: p.current.Pos, + IsRevoke: false, + } + + // Skip all tokens until end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + p.nextToken() + } + + return grant +} + +// parseRevoke handles REVOKE statements +func (p *Parser) parseRevoke() *ast.GrantQuery { + grant := &ast.GrantQuery{ + Position: p.current.Pos, + IsRevoke: true, + } + + // Skip all tokens until end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + p.nextToken() + } + + return grant +} diff --git a/parser/testdata/01018_ddl_dictionaries_create/metadata.json b/parser/testdata/01018_ddl_dictionaries_create/metadata.json index 5422490887..c100a57140 100644 --- a/parser/testdata/01018_ddl_dictionaries_create/metadata.json +++ b/parser/testdata/01018_ddl_dictionaries_create/metadata.json @@ -1,21 +1,14 @@ { "explain_todo": { "stmt11": true, - "stmt12": true, "stmt13": true, - "stmt14": true, "stmt17": true, "stmt18": true, - "stmt19": true, "stmt22": true, "stmt23": true, - "stmt24": true, "stmt28": true, - "stmt29": true, "stmt35": true, - "stmt36": true, "stmt37": true, - "stmt38": true, "stmt43": true, "stmt47": true, "stmt48": true, diff --git a/parser/testdata/01048_exists_query/metadata.json b/parser/testdata/01048_exists_query/metadata.json index 45d43834ca..631cf49570 100644 --- a/parser/testdata/01048_exists_query/metadata.json +++ b/parser/testdata/01048_exists_query/metadata.json @@ -1,27 +1,11 @@ { "explain_todo": { - "stmt11": true, "stmt12": true, - "stmt15": true, - "stmt19": true, "stmt22": true, "stmt23": true, "stmt24": true, "stmt25": true, - "stmt28": true, - "stmt3": true, - "stmt33": true, "stmt34": true, - "stmt35": true, - "stmt36": true, - "stmt37": true, - "stmt38": true, - "stmt40": true, - "stmt41": true, - "stmt42": true, - "stmt43": true, - "stmt48": true, - "stmt5": true, - "stmt7": true + "stmt35": true } } diff --git a/parser/testdata/01074_partial_revokes/metadata.json b/parser/testdata/01074_partial_revokes/metadata.json index 651e92deb1..0967ef424b 100644 --- a/parser/testdata/01074_partial_revokes/metadata.json +++ b/parser/testdata/01074_partial_revokes/metadata.json @@ -1,62 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt15": true, - "stmt16": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt22": true, - "stmt23": true, - "stmt25": true, - "stmt26": true, - "stmt27": true, - "stmt29": true, - "stmt30": true, - "stmt32": true, - "stmt33": true, - "stmt34": true, - "stmt35": true, - "stmt36": true, - "stmt39": true, - "stmt4": true, - "stmt40": true, - "stmt42": true, - "stmt43": true, - "stmt44": true, - "stmt45": true, - "stmt47": true, - "stmt48": true, - "stmt5": true, - "stmt50": true, - "stmt51": true, - "stmt52": true, - "stmt53": true, - "stmt55": true, - "stmt56": true, - "stmt58": true, - "stmt59": true, - "stmt6": true, - "stmt60": true, - "stmt63": true, - "stmt64": true, - "stmt66": true, - "stmt67": true, - "stmt68": true, - "stmt70": true, - "stmt71": true, - "stmt73": true, - "stmt74": true, - "stmt75": true, - "stmt77": true, - "stmt78": true, - "stmt8": true, - "stmt80": true, - "stmt81": true, - "stmt82": true, - "stmt83": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/01110_dictionary_layout_without_arguments/metadata.json b/parser/testdata/01110_dictionary_layout_without_arguments/metadata.json index 5e11b65ab1..21be370037 100644 --- a/parser/testdata/01110_dictionary_layout_without_arguments/metadata.json +++ b/parser/testdata/01110_dictionary_layout_without_arguments/metadata.json @@ -4,7 +4,6 @@ "stmt4": true, "stmt5": true, "stmt7": true, - "stmt8": true, - "stmt9": true + "stmt8": true } } diff --git a/parser/testdata/01292_create_user/metadata.json b/parser/testdata/01292_create_user/metadata.json index bd80a6826c..407f7c2b72 100644 --- a/parser/testdata/01292_create_user/metadata.json +++ b/parser/testdata/01292_create_user/metadata.json @@ -39,7 +39,6 @@ "stmt151": true, "stmt152": true, "stmt153": true, - "stmt154": true, "stmt155": true, "stmt156": true, "stmt157": true, @@ -58,20 +57,14 @@ "stmt170": true, "stmt171": true, "stmt172": true, - "stmt176": true, "stmt177": true, - "stmt178": true, "stmt18": true, - "stmt181": true, "stmt182": true, - "stmt183": true, "stmt186": true, - "stmt187": true, "stmt189": true, "stmt196": true, "stmt197": true, "stmt198": true, - "stmt199": true, "stmt200": true, "stmt201": true, "stmt205": true, diff --git a/parser/testdata/01702_system_query_log/metadata.json b/parser/testdata/01702_system_query_log/metadata.json index e353aed2a5..d19a050dac 100644 --- a/parser/testdata/01702_system_query_log/metadata.json +++ b/parser/testdata/01702_system_query_log/metadata.json @@ -9,7 +9,6 @@ "stmt18": true, "stmt19": true, "stmt20": true, - "stmt21": true, "stmt24": true, "stmt26": true, "stmt27": true, @@ -45,10 +44,6 @@ "stmt59": true, "stmt6": true, "stmt60": true, - "stmt62": true, - "stmt63": true, - "stmt65": true, - "stmt66": true, "stmt68": true, "stmt69": true, "stmt7": true, diff --git a/parser/testdata/01999_grant_with_replace/metadata.json b/parser/testdata/01999_grant_with_replace/metadata.json index 347e414bd8..a74071bf7a 100644 --- a/parser/testdata/01999_grant_with_replace/metadata.json +++ b/parser/testdata/01999_grant_with_replace/metadata.json @@ -1,40 +1,11 @@ { "explain_todo": { - "stmt10": true, - "stmt12": true, - "stmt13": true, - "stmt15": true, - "stmt16": true, - "stmt18": true, - "stmt20": true, - "stmt21": true, - "stmt23": true, - "stmt24": true, "stmt26": true, "stmt27": true, - "stmt28": true, - "stmt29": true, "stmt3": true, - "stmt30": true, - "stmt32": true, - "stmt33": true, - "stmt35": true, - "stmt36": true, - "stmt38": true, - "stmt39": true, - "stmt41": true, - "stmt42": true, - "stmt44": true, - "stmt45": true, "stmt47": true, "stmt48": true, - "stmt49": true, - "stmt5": true, - "stmt50": true, "stmt52": true, - "stmt53": true, - "stmt6": true, - "stmt7": true, - "stmt9": true + "stmt53": true } } diff --git a/parser/testdata/02024_create_dictionary_with_comment/metadata.json b/parser/testdata/02024_create_dictionary_with_comment/metadata.json index 0f293987f1..3a06a4a1ac 100644 --- a/parser/testdata/02024_create_dictionary_with_comment/metadata.json +++ b/parser/testdata/02024_create_dictionary_with_comment/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt5": true, - "stmt6": true + "stmt5": true } } diff --git a/parser/testdata/03230_show_create_query_identifier_quoting_style/metadata.json b/parser/testdata/03230_show_create_query_identifier_quoting_style/metadata.json index 57c839677b..cd7763da2a 100644 --- a/parser/testdata/03230_show_create_query_identifier_quoting_style/metadata.json +++ b/parser/testdata/03230_show_create_query_identifier_quoting_style/metadata.json @@ -4,7 +4,6 @@ "stmt12": true, "stmt19": true, "stmt21": true, - "stmt22": true, "stmt24": true, "stmt25": true, "stmt26": true, diff --git a/parser/testdata/03594_system_grants_parameters/metadata.json b/parser/testdata/03594_system_grants_parameters/metadata.json index a9cc4e5a0d..0967ef424b 100644 --- a/parser/testdata/03594_system_grants_parameters/metadata.json +++ b/parser/testdata/03594_system_grants_parameters/metadata.json @@ -1,10 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03727_alter_with_localhost_remote/metadata.json b/parser/testdata/03727_alter_with_localhost_remote/metadata.json index 661bded8e9..0967ef424b 100644 --- a/parser/testdata/03727_alter_with_localhost_remote/metadata.json +++ b/parser/testdata/03727_alter_with_localhost_remote/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{}