From 4f3870cd154bc17b16ebf24328bb103d9d027c88 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:12:30 +0000 Subject: [PATCH 01/22] Fix JSON path type accessor parsing and projection ORDER BY - Add handling for `.:TypeName` syntax in parseArrayAccess after `[]` This allows parsing expressions like `json.c[].d.:Int64` - Fix projection ORDER BY to use parseExpression instead of just reading a single identifier, allowing qualified identifiers like `json.a`, `t.a`, and `json.c[].d.:Int64` Fixes 22 statements in 03464_projections_with_subcolumns and additional statements in other tests. --- parser/expression.go | 9 +++++++ parser/parser.go | 11 +++----- .../00515_enhanced_time_zones/metadata.json | 7 +---- .../metadata.json | 6 +---- .../metadata.json | 6 +---- .../metadata.json | 2 -- .../metadata.json | 12 --------- .../03443_projection_sparse/metadata.json | 6 +---- .../metadata.json | 27 +------------------ .../metadata.json | 2 -- 10 files changed, 18 insertions(+), 70 deletions(-) diff --git a/parser/expression.go b/parser/expression.go index 636d38983e..d6756394fc 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -1857,6 +1857,15 @@ func (p *Parser) parseArrayAccess(left ast.Expression) ast.Expression { } else { break } + } else if p.currentIs(token.COLON) { + // JSON subcolumn type accessor: json.field.:`TypeName` + p.nextToken() // skip : + typePart := ":" + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() || p.currentIs(token.STRING) { + typePart += "`" + p.current.Value + "`" + p.nextToken() + } + ident.Parts = append(ident.Parts, typePart) } else if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { ident.Parts = append(ident.Parts, p.current.Value) p.nextToken() diff --git a/parser/parser.go b/parser/parser.go index bdd6bbc125..fe4ad052ba 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -5343,14 +5343,11 @@ func (p *Parser) parseProjection() *ast.Projection { if p.currentIs(token.BY) { p.nextToken() // BY } - // Parse ORDER BY columns (comma-separated identifiers) + // Parse ORDER BY columns (comma-separated expressions) for !p.currentIs(token.EOF) && !p.currentIs(token.RPAREN) { - if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { - proj.Select.OrderBy = append(proj.Select.OrderBy, &ast.Identifier{ - Position: p.current.Pos, - Parts: []string{p.current.Value}, - }) - p.nextToken() + expr := p.parseExpression(LOWEST) + if expr != nil { + proj.Select.OrderBy = append(proj.Select.OrderBy, expr) } else { break } diff --git a/parser/testdata/00515_enhanced_time_zones/metadata.json b/parser/testdata/00515_enhanced_time_zones/metadata.json index f881a8c517..0967ef424b 100644 --- a/parser/testdata/00515_enhanced_time_zones/metadata.json +++ b/parser/testdata/00515_enhanced_time_zones/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt44": true, - "stmt49": true - } -} +{} diff --git a/parser/testdata/01710_normal_projection_format/metadata.json b/parser/testdata/01710_normal_projection_format/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/01710_normal_projection_format/metadata.json +++ b/parser/testdata/01710_normal_projection_format/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} diff --git a/parser/testdata/02816_check_projection_metadata/metadata.json b/parser/testdata/02816_check_projection_metadata/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/02816_check_projection_metadata/metadata.json +++ b/parser/testdata/02816_check_projection_metadata/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/02997_projections_formatting/metadata.json b/parser/testdata/02997_projections_formatting/metadata.json index 6ed702cc94..9a8cc69c0b 100644 --- a/parser/testdata/02997_projections_formatting/metadata.json +++ b/parser/testdata/02997_projections_formatting/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { - "stmt1": true, "stmt2": true, - "stmt3": true, "stmt4": true } } diff --git a/parser/testdata/03208_array_of_json_read_subcolumns_2_memory/metadata.json b/parser/testdata/03208_array_of_json_read_subcolumns_2_memory/metadata.json index 90fa5ee0a1..3a06a4a1ac 100644 --- a/parser/testdata/03208_array_of_json_read_subcolumns_2_memory/metadata.json +++ b/parser/testdata/03208_array_of_json_read_subcolumns_2_memory/metadata.json @@ -1,17 +1,5 @@ { "explain_todo": { - "stmt18": true, - "stmt20": true, - "stmt23": true, - "stmt24": true, - "stmt27": true, - "stmt28": true, - "stmt30": true, - "stmt32": true, - "stmt35": true, - "stmt36": true, - "stmt39": true, - "stmt40": true, "stmt5": true } } diff --git a/parser/testdata/03443_projection_sparse/metadata.json b/parser/testdata/03443_projection_sparse/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03443_projection_sparse/metadata.json +++ b/parser/testdata/03443_projection_sparse/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/03464_projections_with_subcolumns/metadata.json b/parser/testdata/03464_projections_with_subcolumns/metadata.json index 86dd984b2e..0967ef424b 100644 --- a/parser/testdata/03464_projections_with_subcolumns/metadata.json +++ b/parser/testdata/03464_projections_with_subcolumns/metadata.json @@ -1,26 +1 @@ -{ - "explain_todo": { - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt24": true, - "stmt25": true, - "stmt26": true, - "stmt31": true, - "stmt33": true, - "stmt35": true, - "stmt37": true, - "stmt38": true, - "stmt39": true, - "stmt40": true, - "stmt41": true, - "stmt42": true, - "stmt43": true, - "stmt44": true, - "stmt45": true, - "stmt5": true, - "stmt52": true, - "stmt53": true, - "stmt54": true - } -} +{} diff --git a/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json b/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json index a3aec753ae..2712c834b2 100644 --- a/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json +++ b/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json @@ -1,10 +1,8 @@ { "explain_todo": { - "stmt10": true, "stmt11": true, "stmt15": true, "stmt20": true, - "stmt23": true, "stmt24": true, "stmt27": true, "stmt31": true From da36378dcc26f05019cda897c591cb4d2c4bcb29 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:23:32 +0000 Subject: [PATCH 02/22] Add FETCH PARTITION support and fix PARTITION ALL child count - Add AlterFetchPartition and AlterMovePartition types to AST - Add FromPath field to AlterCommand for FETCH PARTITION FROM - Implement FETCH PARTITION parsing in parseAlterCommand - Fix children count for PARTITION ALL (now counts as 1 child) - Add explain support for FETCH_PARTITION and MOVE_PARTITION Fixes stmt42 in 00753_alter_attach and other FETCH PARTITION tests. --- ast/ast.go | 3 +++ internal/explain/statements.go | 9 +++------ parser/parser.go | 15 +++++++++++++++ .../testdata/00753_alter_attach/metadata.json | 18 +----------------- .../testdata/01015_attach_part/metadata.json | 3 +-- .../metadata.json | 2 -- .../metadata.json | 6 +----- .../metadata.json | 1 - .../metadata.json | 6 +----- 9 files changed, 25 insertions(+), 38 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index c69df6f960..829e29e5c5 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -529,6 +529,7 @@ type AlterCommand struct { ConstraintName string `json:"constraint_name,omitempty"` Partition Expression `json:"partition,omitempty"` FromTable string `json:"from_table,omitempty"` + FromPath string `json:"from_path,omitempty"` // For FETCH PARTITION FROM TTL *TTLClause `json:"ttl,omitempty"` Settings []*SettingExpr `json:"settings,omitempty"` Where Expression `json:"where,omitempty"` // For DELETE WHERE @@ -593,6 +594,8 @@ const ( AlterDetachPartition AlterCommandType = "DETACH_PARTITION" AlterAttachPartition AlterCommandType = "ATTACH_PARTITION" AlterReplacePartition AlterCommandType = "REPLACE_PARTITION" + AlterFetchPartition AlterCommandType = "FETCH_PARTITION" + AlterMovePartition AlterCommandType = "MOVE_PARTITION" AlterFreezePartition AlterCommandType = "FREEZE_PARTITION" AlterFreeze AlterCommandType = "FREEZE" AlterDeleteWhere AlterCommandType = "DELETE_WHERE" diff --git a/internal/explain/statements.go b/internal/explain/statements.go index cf7b187357..e9a5eb6561 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -1091,7 +1091,7 @@ func explainAlterCommand(sb *strings.Builder, cmd *ast.AlterCommand, indent stri case ast.AlterModifySetting: fmt.Fprintf(sb, "%s Set\n", indent) case ast.AlterDropPartition, ast.AlterDetachPartition, ast.AlterAttachPartition, - ast.AlterReplacePartition, ast.AlterFreezePartition: + ast.AlterReplacePartition, ast.AlterFetchPartition, ast.AlterMovePartition, ast.AlterFreezePartition: if cmd.Partition != nil { // PARTITION ALL is shown as Partition_ID (empty) in EXPLAIN AST if ident, ok := cmd.Partition.(*ast.Identifier); ok && strings.ToUpper(ident.Name()) == "ALL" { @@ -1284,12 +1284,9 @@ func countAlterCommandChildren(cmd *ast.AlterCommand) int { case ast.AlterModifySetting: children = 1 case ast.AlterDropPartition, ast.AlterDetachPartition, ast.AlterAttachPartition, - ast.AlterReplacePartition, ast.AlterFreezePartition: + ast.AlterReplacePartition, ast.AlterFetchPartition, ast.AlterMovePartition, ast.AlterFreezePartition: if cmd.Partition != nil { - // PARTITION ALL doesn't count as a child (shown as Partition_ID empty) - if ident, ok := cmd.Partition.(*ast.Identifier); !ok || strings.ToUpper(ident.Name()) != "ALL" { - children++ - } + children++ } case ast.AlterFreeze: // No children diff --git a/parser/parser.go b/parser/parser.go index fe4ad052ba..1c56d15618 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -4179,6 +4179,21 @@ func (p *Parser) parseAlterCommand() *ast.AlterCommand { } } } + case token.FETCH: + p.nextToken() + if p.currentIs(token.PARTITION) { + cmd.Type = ast.AlterFetchPartition + p.nextToken() + cmd.Partition = p.parseExpression(LOWEST) + // FROM path + if p.currentIs(token.FROM) { + p.nextToken() + if p.currentIs(token.STRING) { + cmd.FromPath = p.current.Value + p.nextToken() + } + } + } case token.DELETE: // DELETE WHERE condition - mutation to delete rows cmd.Type = ast.AlterDeleteWhere diff --git a/parser/testdata/00753_alter_attach/metadata.json b/parser/testdata/00753_alter_attach/metadata.json index 407a67123a..249a399fbe 100644 --- a/parser/testdata/00753_alter_attach/metadata.json +++ b/parser/testdata/00753_alter_attach/metadata.json @@ -1,25 +1,9 @@ { "explain_todo": { - "stmt14": true, - "stmt22": true, - "stmt34": true, - "stmt42": true, - "stmt51": true, "stmt52": true, "stmt53": true, "stmt54": true, "stmt55": true, - "stmt56": true, - "stmt58": true, - "stmt63": true, - "stmt65": true, - "stmt69": true, - "stmt71": true, - "stmt73": true, - "stmt77": true, - "stmt79": true, - "stmt83": true, - "stmt85": true, - "stmt87": true + "stmt58": true } } diff --git a/parser/testdata/01015_attach_part/metadata.json b/parser/testdata/01015_attach_part/metadata.json index 84f57deaf2..6bf8d5b80a 100644 --- a/parser/testdata/01015_attach_part/metadata.json +++ b/parser/testdata/01015_attach_part/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt5": true, - "stmt7": true, - "stmt9": true + "stmt7": true } } diff --git a/parser/testdata/01166_truncate_multiple_partitions/metadata.json b/parser/testdata/01166_truncate_multiple_partitions/metadata.json index ad5752d0f4..d0821ccce9 100644 --- a/parser/testdata/01166_truncate_multiple_partitions/metadata.json +++ b/parser/testdata/01166_truncate_multiple_partitions/metadata.json @@ -3,13 +3,11 @@ "stmt10": true, "stmt11": true, "stmt17": true, - "stmt20": true, "stmt22": true, "stmt23": true, "stmt24": true, "stmt25": true, "stmt3": true, - "stmt6": true, "stmt8": true, "stmt9": true } diff --git a/parser/testdata/01650_fetch_patition_with_macro_in_zk_path_long/metadata.json b/parser/testdata/01650_fetch_patition_with_macro_in_zk_path_long/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/01650_fetch_patition_with_macro_in_zk_path_long/metadata.json +++ b/parser/testdata/01650_fetch_patition_with_macro_in_zk_path_long/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/03203_drop_detached_partition_all/metadata.json b/parser/testdata/03203_drop_detached_partition_all/metadata.json index 6bf8d5b80a..b563327205 100644 --- a/parser/testdata/03203_drop_detached_partition_all/metadata.json +++ b/parser/testdata/03203_drop_detached_partition_all/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt5": true, "stmt7": true } } diff --git a/parser/testdata/03350_alter_table_fetch_partition_thread_pool/metadata.json b/parser/testdata/03350_alter_table_fetch_partition_thread_pool/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/03350_alter_table_fetch_partition_thread_pool/metadata.json +++ b/parser/testdata/03350_alter_table_fetch_partition_thread_pool/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} From b3b61bf5f77ad9165d1112562e4bbd52e3867563 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:32:48 +0000 Subject: [PATCH 03/22] Allow keywords as table aliases without AS keyword When parsing table expressions, allow keywords (like FIRST, SECOND_) to be used as table aliases without requiring the AS keyword, as long as the keyword is not a clause keyword. Added more keywords to isKeywordForClause() to prevent them from being incorrectly parsed as aliases: - ARRAY (for ARRAY JOIN) - WINDOW (for window functions) - WITH (for WITH clause/CTEs) - INTERSECT (for set operations) - SELECT (for FROM...SELECT syntax) - TOTALS (for WITH TOTALS) Fixes 21 statements in 00674_join_on_syntax test. --- parser/parser.go | 12 ++++++--- .../00674_join_on_syntax/metadata.json | 26 +------------------ 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 1c56d15618..598bc40741 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1116,14 +1116,15 @@ func (p *Parser) parseTableExpression() *ast.TableExpression { } } - // Handle alias (keywords like LEFT, RIGHT can be used as aliases after AS) + // Handle alias (keywords like LEFT, RIGHT, FIRST can be used as aliases after AS, + // or without AS if they're not clause keywords) if p.currentIs(token.AS) { p.nextToken() if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { expr.Alias = p.current.Value p.nextToken() } - } else if p.currentIs(token.IDENT) && !p.isKeywordForClause() { + } else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && !p.isKeywordForClause() { expr.Alias = p.current.Value p.nextToken() } @@ -1137,7 +1138,12 @@ func (p *Parser) isKeywordForClause() bool { token.OFFSET, token.UNION, token.EXCEPT, token.SETTINGS, token.FORMAT, token.PREWHERE, token.JOIN, token.LEFT, token.RIGHT, token.INNER, token.FULL, token.CROSS, token.PASTE, token.ON, token.USING, token.GLOBAL, - token.ANY, token.ALL, token.SEMI, token.ANTI, token.ASOF: + token.ANY, token.ALL, token.SEMI, token.ANTI, token.ASOF, token.ARRAY, + token.WINDOW, token.WITH, token.INTERSECT, token.SELECT: + return true + } + // Handle TOTALS as a clause keyword when used in "WITH TOTALS" + if p.current.Token == token.IDENT && strings.ToUpper(p.current.Value) == "TOTALS" { return true } return false diff --git a/parser/testdata/00674_join_on_syntax/metadata.json b/parser/testdata/00674_join_on_syntax/metadata.json index ef51318230..0967ef424b 100644 --- a/parser/testdata/00674_join_on_syntax/metadata.json +++ b/parser/testdata/00674_join_on_syntax/metadata.json @@ -1,25 +1 @@ -{ - "explain_todo": { - "stmt39": true, - "stmt40": true, - "stmt41": true, - "stmt42": true, - "stmt43": true, - "stmt44": true, - "stmt45": true, - "stmt46": true, - "stmt47": true, - "stmt48": true, - "stmt49": true, - "stmt50": true, - "stmt51": true, - "stmt52": true, - "stmt53": true, - "stmt54": true, - "stmt55": true, - "stmt83": true, - "stmt84": true, - "stmt85": true, - "stmt86": true - } -} +{} From 63368b8859c9748fe477392f543ded982ce3f940 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:38:03 +0000 Subject: [PATCH 04/22] Add MySQL INT type display width and SHOW CREATE TEMPORARY TABLE support - Handle MySQL-compatible INT types (INT, TINYINT, SMALLINT, MEDIUMINT, BIGINT) by ignoring the display width parameter like INT(11) - Handle UNSIGNED and SIGNED modifiers for INT types, appending them to the type name - Fix SHOW CREATE TEMPORARY TABLE parsing to skip TEMPORARY keyword Fixes 20 statements in 02271_int_sql_compatibility test. --- parser/parser.go | 28 ++++++++++++++++++- .../02271_int_sql_compatibility/metadata.json | 25 +---------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 598bc40741..d1ad34dd1e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -3226,6 +3226,29 @@ func (p *Parser) parseDataType() *ast.DataType { } p.nextToken() + // For MySQL-compatible INT types, handle display width and UNSIGNED/SIGNED + upperName := strings.ToUpper(dt.Name) + isMySQLIntType := upperName == "INT" || upperName == "TINYINT" || upperName == "SMALLINT" || + upperName == "MEDIUMINT" || upperName == "BIGINT" + + if isMySQLIntType && p.currentIs(token.LPAREN) { + // Skip the display width parameter (e.g., INT(11)) + p.nextToken() // skip ( + for !p.currentIs(token.RPAREN) && !p.currentIs(token.EOF) { + p.nextToken() + } + p.expect(token.RPAREN) + } + + // Handle UNSIGNED/SIGNED modifiers for MySQL INT types + if isMySQLIntType && p.currentIs(token.IDENT) { + modifier := strings.ToUpper(p.current.Value) + if modifier == "UNSIGNED" || modifier == "SIGNED" { + dt.Name = dt.Name + " " + p.current.Value + p.nextToken() + } + } + // Parse type parameters if p.currentIs(token.LPAREN) { dt.HasParentheses = true @@ -4426,7 +4449,10 @@ func (p *Parser) parseShow() ast.Statement { } } else { show.ShowType = ast.ShowCreate - // Handle SHOW CREATE TABLE, etc. + // Handle SHOW CREATE TABLE, SHOW CREATE TEMPORARY TABLE, etc. + if p.currentIs(token.TEMPORARY) { + p.nextToken() + } if p.currentIs(token.TABLE) { p.nextToken() } diff --git a/parser/testdata/02271_int_sql_compatibility/metadata.json b/parser/testdata/02271_int_sql_compatibility/metadata.json index ce72cf920f..0967ef424b 100644 --- a/parser/testdata/02271_int_sql_compatibility/metadata.json +++ b/parser/testdata/02271_int_sql_compatibility/metadata.json @@ -1,24 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt2": true, - "stmt20": true, - "stmt3": true, - "stmt4": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} From f9c043fbbfe444b8007083376381a48a7b6d25e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:43:09 +0000 Subject: [PATCH 05/22] Add ANY/ALL subquery operator normalization - Map anyMatch to in (for expr == any subquery) - Map allMatch to notIn (for expr != all subquery) These SQL standard comparison operators with subqueries are normalized to ClickHouse's IN/NOT IN functions in EXPLAIN AST output. Fixes 5 statements in 02007_test_any_all_operators test. --- internal/explain/format.go | 3 +++ parser/testdata/02007_test_any_all_operators/metadata.json | 7 +------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index 1ab3eb49ad..2c3558a0bc 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -294,6 +294,9 @@ func NormalizeFunctionName(name string) string { "greatest": "greatest", "least": "least", "concat_ws": "concat", + // SQL standard ANY/ALL subquery operators + "anymatch": "in", + "allmatch": "notIn", } if n, ok := normalized[strings.ToLower(name)]; ok { return n diff --git a/parser/testdata/02007_test_any_all_operators/metadata.json b/parser/testdata/02007_test_any_all_operators/metadata.json index ce72cf920f..4995e0b439 100644 --- a/parser/testdata/02007_test_any_all_operators/metadata.json +++ b/parser/testdata/02007_test_any_all_operators/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt1": true, "stmt10": true, "stmt11": true, "stmt12": true, @@ -11,14 +10,10 @@ "stmt17": true, "stmt18": true, "stmt19": true, - "stmt2": true, "stmt20": true, - "stmt3": true, - "stmt4": true, "stmt5": true, "stmt6": true, "stmt7": true, - "stmt8": true, - "stmt9": true + "stmt8": true } } From af0114b6f871d2b1171b91cadd8d542e9ba5956b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:47:06 +0000 Subject: [PATCH 06/22] Add DATE/TIMESTAMP/TIME typed literal support Parse SQL standard typed literals like: - DATE '2022-01-01' as toDate('2022-01-01') - TIMESTAMP '...' as toDateTime('...') - TIME '...' as toTime('...') Fixes stmt14 in 02160_special_functions test. --- parser/expression.go | 23 +++++++++++++++++++ .../02160_special_functions/metadata.json | 1 - 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/parser/expression.go b/parser/expression.go index d6756394fc..1d6b07e1f7 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -440,6 +440,29 @@ func (p *Parser) parseIdentifierOrFunction() ast.Expression { name := p.current.Value p.nextToken() + // Check for typed literals: DATE '...', TIMESTAMP '...', TIME '...' + // These are converted to toDate(), toDateTime(), toTime() function calls + upperName := strings.ToUpper(name) + if p.currentIs(token.STRING) && (upperName == "DATE" || upperName == "TIMESTAMP" || upperName == "TIME") { + fnName := "toDate" + if upperName == "TIMESTAMP" { + fnName = "toDateTime" + } else if upperName == "TIME" { + fnName = "toTime" + } + strLit := &ast.Literal{ + Position: p.current.Pos, + Type: "String", + Value: p.current.Value, + } + p.nextToken() + return &ast.FunctionCall{ + Position: pos, + Name: fnName, + Arguments: []ast.Expression{strLit}, + } + } + // Check for MySQL-style @@variable syntax (system variables) // Convert to globalVariable('varname') function call with alias @@varname if strings.HasPrefix(name, "@@") { diff --git a/parser/testdata/02160_special_functions/metadata.json b/parser/testdata/02160_special_functions/metadata.json index 61cf9382a8..6d66df8749 100644 --- a/parser/testdata/02160_special_functions/metadata.json +++ b/parser/testdata/02160_special_functions/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt14": true, "stmt16": true, "stmt18": true, "stmt19": true, From 06d42157e69371ae512067b5f44bec306f9c3b68 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:51:21 +0000 Subject: [PATCH 07/22] Add GROUP BY ALL support - Add GroupByAll field to SelectQuery AST struct - Detect GROUP BY ALL in parser and set the flag - Skip outputting GROUP BY expression list in EXPLAIN for GROUP BY ALL In ClickHouse, GROUP BY ALL is a special syntax that doesn't produce a GROUP BY expression list in the EXPLAIN AST output. Fixes 20 statements in 02459_group_by_all test. --- ast/ast.go | 1 + internal/explain/select.go | 6 ++--- parser/parser.go | 4 +++ .../testdata/02459_group_by_all/metadata.json | 25 +------------------ 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 829e29e5c5..6af9ce4504 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -64,6 +64,7 @@ type SelectQuery struct { PreWhere Expression `json:"prewhere,omitempty"` Where Expression `json:"where,omitempty"` GroupBy []Expression `json:"group_by,omitempty"` + GroupByAll bool `json:"group_by_all,omitempty"` // true if GROUP BY ALL was used GroupingSets bool `json:"grouping_sets,omitempty"` // true if GROUP BY uses GROUPING SETS WithRollup bool `json:"with_rollup,omitempty"` WithCube bool `json:"with_cube,omitempty"` diff --git a/internal/explain/select.go b/internal/explain/select.go index 06651fa6d3..3c30c111aa 100644 --- a/internal/explain/select.go +++ b/internal/explain/select.go @@ -96,8 +96,8 @@ func explainSelectQuery(sb *strings.Builder, n *ast.SelectQuery, indent string, if n.Where != nil { Node(sb, n.Where, depth+1) } - // GROUP BY - if len(n.GroupBy) > 0 { + // GROUP BY (skip for GROUP BY ALL which doesn't output an expression list) + if len(n.GroupBy) > 0 && !n.GroupByAll { fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.GroupBy)) for _, g := range n.GroupBy { if n.GroupingSets { @@ -327,7 +327,7 @@ func countSelectQueryChildren(n *ast.SelectQuery) int { if n.Where != nil { count++ } - if len(n.GroupBy) > 0 { + if len(n.GroupBy) > 0 && !n.GroupByAll { count++ } if n.Having != nil { diff --git a/parser/parser.go b/parser/parser.go index d1ad34dd1e..db20f4d7b5 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -550,6 +550,10 @@ func (p *Parser) parseSelect() *ast.SelectQuery { sel.GroupBy = p.parseExpressionList() p.expect(token.RPAREN) sel.WithCube = true + } else if p.currentIs(token.ALL) { + // GROUP BY ALL - special ClickHouse syntax + sel.GroupByAll = true + sel.GroupBy = p.parseExpressionList() // Still parse it, but mark as GroupByAll } else { sel.GroupBy = p.parseExpressionList() } diff --git a/parser/testdata/02459_group_by_all/metadata.json b/parser/testdata/02459_group_by_all/metadata.json index 929f4551b7..0967ef424b 100644 --- a/parser/testdata/02459_group_by_all/metadata.json +++ b/parser/testdata/02459_group_by_all/metadata.json @@ -1,24 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt21": true, - "stmt22": true, - "stmt23": true, - "stmt24": true, - "stmt4": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} From 4aa3243505dcf88527882796387e4ff04b6ca0fd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 14:52:38 +0000 Subject: [PATCH 08/22] Update 01099 test metadata (19 more tests pass with DATE literal support) --- .../metadata.json | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/parser/testdata/01099_operators_date_and_timestamp/metadata.json b/parser/testdata/01099_operators_date_and_timestamp/metadata.json index 950a708e32..85cc99e9fa 100644 --- a/parser/testdata/01099_operators_date_and_timestamp/metadata.json +++ b/parser/testdata/01099_operators_date_and_timestamp/metadata.json @@ -1,24 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt21": true, - "stmt22": true, - "stmt31": true, - "stmt34": true, - "stmt4": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true + "stmt34": true } } From 3ed41f66dca5f7ddef267e6c7a778172d25954a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 15:09:52 +0000 Subject: [PATCH 09/22] Add SKIP and SKIP REGEXP support for JSON type parameters This adds proper parsing and formatting for SKIP clauses in JSON type parameters, including: - SKIP path (e.g., SKIP a.b for dotted paths) - SKIP REGEXP 'pattern' (for regex-based path matching) These are used in JSON type casts like: json::JSON(SKIP a.b, max_dynamic_paths=2) json::JSON(SKIP REGEXP '.*a.*', max_dynamic_paths=2) --- internal/explain/format.go | 13 ++++ parser/parser.go | 59 ++++++++++++++++++- .../metadata.json | 1 - .../metadata.json | 6 +- .../01095_tpch_like_smoke/metadata.json | 17 +----- .../metadata.json | 6 +- .../metadata.json | 7 +-- .../02184_default_table_engine/metadata.json | 2 - .../metadata.json | 7 +-- .../metadata.json | 7 +-- .../metadata.json | 3 +- .../03205_json_cast_from_string/metadata.json | 8 +-- .../metadata.json | 7 +-- .../metadata.json | 6 +- .../metadata.json | 6 +- .../03272_json_to_json_cast_3/metadata.json | 25 +------- .../metadata.json | 2 +- .../metadata.json | 9 +-- .../03370_join_identifiers/metadata.json | 7 +-- .../testdata/03401_remote_bool/metadata.json | 6 +- .../metadata.json | 6 +- .../03453_group_by_all_grouping/metadata.json | 2 +- .../03532_json_dynamic_updates/metadata.json | 8 --- .../03577_temporary_table_as/metadata.json | 6 +- .../03583_rewrite_in_to_join/metadata.json | 6 +- .../03639_hash_of_json_column/metadata.json | 6 +- 26 files changed, 92 insertions(+), 146 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index 2c3558a0bc..d0db04bdcd 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -237,6 +237,19 @@ func FormatDataType(dt *ast.DataType) string { } else if binExpr, ok := p.(*ast.BinaryExpr); ok { // Binary expression (e.g., 'hello' = 1 for Enum types) params = append(params, formatBinaryExprForType(binExpr)) + } else if fn, ok := p.(*ast.FunctionCall); ok { + // Function call (e.g., SKIP for JSON types) + if fn.Name == "SKIP" && len(fn.Arguments) > 0 { + if ident, ok := fn.Arguments[0].(*ast.Identifier); ok { + params = append(params, "SKIP "+ident.Name()) + } + } else if fn.Name == "SKIP REGEXP" && len(fn.Arguments) > 0 { + if lit, ok := fn.Arguments[0].(*ast.Literal); ok { + params = append(params, fmt.Sprintf("SKIP REGEXP \\\\\\'%s\\\\\\'", lit.Value)) + } + } else { + params = append(params, fmt.Sprintf("%v", p)) + } } else { params = append(params, fmt.Sprintf("%v", p)) } diff --git a/parser/parser.go b/parser/parser.go index db20f4d7b5..2a45bdbb80 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -3266,6 +3266,64 @@ func (p *Parser) parseDataType() *ast.DataType { // Parse type parameters, but stop on keywords that can't be part of type params for !p.currentIs(token.RPAREN) && !p.currentIs(token.EOF) && !p.currentIs(token.COLLATE) { + var param ast.Expression + + // Special handling for SKIP in JSON/OBJECT types: SKIP path or SKIP REGEXP 'pattern' + if isObjectType && (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && strings.ToUpper(p.current.Value) == "SKIP" { + pos := p.current.Pos + p.nextToken() // consume SKIP + + // Check for SKIP REGEXP 'pattern' + if p.currentIs(token.REGEXP) || (p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "REGEXP") { + p.nextToken() // consume REGEXP + // Parse the pattern string + if p.currentIs(token.STRING) { + pattern := p.current.Value + p.nextToken() + param = &ast.FunctionCall{ + Position: pos, + Name: "SKIP REGEXP", + Arguments: []ast.Expression{&ast.Literal{Position: pos, Value: pattern, Type: ast.LiteralString}}, + } + } + } else { + // Parse dotted path: a, a.b, a.b.c, etc. + var pathParts []string + for { + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + pathParts = append(pathParts, p.current.Value) + p.nextToken() + } + if p.currentIs(token.DOT) { + p.nextToken() // consume dot + } else { + break + } + } + if len(pathParts) > 0 { + param = &ast.FunctionCall{ + Position: pos, + Name: "SKIP", + Arguments: []ast.Expression{&ast.Identifier{Position: pos, Parts: pathParts}}, + } + } + } + // Wrap in ObjectTypeArgument + if param != nil { + param = &ast.ObjectTypeArgument{ + Position: param.Pos(), + Expr: param, + } + dt.Parameters = append(dt.Parameters, param) + } + if p.currentIs(token.COMMA) { + p.nextToken() + } else { + break + } + continue + } + // Check if this is a named parameter: identifier followed by a type name // e.g., "a UInt32" where "a" is the name and "UInt32" is the type isNamedParam := false @@ -3291,7 +3349,6 @@ func (p *Parser) parseDataType() *ast.DataType { } } - var param ast.Expression if isNamedParam { // Parse as name + type pair pos := p.current.Pos diff --git a/parser/testdata/00564_temporary_table_management/metadata.json b/parser/testdata/00564_temporary_table_management/metadata.json index 2301576872..d5e9483c45 100644 --- a/parser/testdata/00564_temporary_table_management/metadata.json +++ b/parser/testdata/00564_temporary_table_management/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt3": true, - "stmt4": true, "stmt5": true, "stmt7": true } diff --git a/parser/testdata/00688_low_cardinality_nullable_cast/metadata.json b/parser/testdata/00688_low_cardinality_nullable_cast/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/00688_low_cardinality_nullable_cast/metadata.json +++ b/parser/testdata/00688_low_cardinality_nullable_cast/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/01095_tpch_like_smoke/metadata.json b/parser/testdata/01095_tpch_like_smoke/metadata.json index 7a435f68f3..0967ef424b 100644 --- a/parser/testdata/01095_tpch_like_smoke/metadata.json +++ b/parser/testdata/01095_tpch_like_smoke/metadata.json @@ -1,16 +1 @@ -{ - "explain_todo": { - "stmt20": true, - "stmt24": true, - "stmt26": true, - "stmt28": true, - "stmt30": true, - "stmt32": true, - "stmt34": true, - "stmt38": true, - "stmt42": true, - "stmt46": true, - "stmt49": true, - "stmt61": true - } -} +{} diff --git a/parser/testdata/01268_data_numeric_parameters/metadata.json b/parser/testdata/01268_data_numeric_parameters/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/01268_data_numeric_parameters/metadata.json +++ b/parser/testdata/01268_data_numeric_parameters/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/01602_temporary_table_in_system_tables/metadata.json b/parser/testdata/01602_temporary_table_in_system_tables/metadata.json index f6d9f2395b..0967ef424b 100644 --- a/parser/testdata/01602_temporary_table_in_system_tables/metadata.json +++ b/parser/testdata/01602_temporary_table_in_system_tables/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt7": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/02184_default_table_engine/metadata.json b/parser/testdata/02184_default_table_engine/metadata.json index 64ab9858cf..f251b24f78 100644 --- a/parser/testdata/02184_default_table_engine/metadata.json +++ b/parser/testdata/02184_default_table_engine/metadata.json @@ -1,11 +1,9 @@ { "explain_todo": { "stmt107": true, - "stmt114": true, "stmt26": true, "stmt56": true, "stmt61": true, - "stmt69": true, "stmt73": true } } diff --git a/parser/testdata/02969_analyzer_eliminate_injective_functions/metadata.json b/parser/testdata/02969_analyzer_eliminate_injective_functions/metadata.json index 7b4ddafa53..0967ef424b 100644 --- a/parser/testdata/02969_analyzer_eliminate_injective_functions/metadata.json +++ b/parser/testdata/02969_analyzer_eliminate_injective_functions/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt3": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/02997_projections_formatting/metadata.json b/parser/testdata/02997_projections_formatting/metadata.json index 9a8cc69c0b..0967ef424b 100644 --- a/parser/testdata/02997_projections_formatting/metadata.json +++ b/parser/testdata/02997_projections_formatting/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt2": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03011_definitive_guide_to_cast/metadata.json b/parser/testdata/03011_definitive_guide_to_cast/metadata.json index 6733b1ee91..48544c5324 100644 --- a/parser/testdata/03011_definitive_guide_to_cast/metadata.json +++ b/parser/testdata/03011_definitive_guide_to_cast/metadata.json @@ -4,7 +4,6 @@ "stmt52": true, "stmt53": true, "stmt84": true, - "stmt86": true, - "stmt88": true + "stmt86": true } } diff --git a/parser/testdata/03205_json_cast_from_string/metadata.json b/parser/testdata/03205_json_cast_from_string/metadata.json index 45ab4e25fe..3a06a4a1ac 100644 --- a/parser/testdata/03205_json_cast_from_string/metadata.json +++ b/parser/testdata/03205_json_cast_from_string/metadata.json @@ -1,11 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true + "stmt5": true } } diff --git a/parser/testdata/03206_is_null_constant_result_old_analyzer_bug/metadata.json b/parser/testdata/03206_is_null_constant_result_old_analyzer_bug/metadata.json index 9022255763..0967ef424b 100644 --- a/parser/testdata/03206_is_null_constant_result_old_analyzer_bug/metadata.json +++ b/parser/testdata/03206_is_null_constant_result_old_analyzer_bug/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt4": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03243_cluster_not_found_column/metadata.json b/parser/testdata/03243_cluster_not_found_column/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03243_cluster_not_found_column/metadata.json +++ b/parser/testdata/03243_cluster_not_found_column/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/03253_group_by_cube_too_many_keys/metadata.json b/parser/testdata/03253_group_by_cube_too_many_keys/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/03253_group_by_cube_too_many_keys/metadata.json +++ b/parser/testdata/03253_group_by_cube_too_many_keys/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/03272_json_to_json_cast_3/metadata.json b/parser/testdata/03272_json_to_json_cast_3/metadata.json index b344e2bbfa..0967ef424b 100644 --- a/parser/testdata/03272_json_to_json_cast_3/metadata.json +++ b/parser/testdata/03272_json_to_json_cast_3/metadata.json @@ -1,24 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt16": true, - "stmt24": true, - "stmt28": true, - "stmt30": true, - "stmt32": true, - "stmt34": true, - "stmt38": true, - "stmt46": true, - "stmt52": true, - "stmt54": true, - "stmt56": true, - "stmt58": true, - "stmt60": true, - "stmt62": true, - "stmt66": true, - "stmt68": true, - "stmt70": true, - "stmt72": true, - "stmt74": true - } -} +{} diff --git a/parser/testdata/03298_analyzer_group_by_all_fix/metadata.json b/parser/testdata/03298_analyzer_group_by_all_fix/metadata.json index 8556c3021f..0967ef424b 100644 --- a/parser/testdata/03298_analyzer_group_by_all_fix/metadata.json +++ b/parser/testdata/03298_analyzer_group_by_all_fix/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt5":true}} +{} diff --git a/parser/testdata/03340_projections_formatting/metadata.json b/parser/testdata/03340_projections_formatting/metadata.json index 9685022035..aeb01f1428 100644 --- a/parser/testdata/03340_projections_formatting/metadata.json +++ b/parser/testdata/03340_projections_formatting/metadata.json @@ -1,13 +1,6 @@ { "explain_todo": { "stmt1": true, - "stmt11": true, - "stmt14": true, - "stmt17": true, - "stmt2": true, - "stmt20": true, - "stmt4": true, - "stmt5": true, - "stmt8": true + "stmt4": true } } diff --git a/parser/testdata/03370_join_identifiers/metadata.json b/parser/testdata/03370_join_identifiers/metadata.json index 7b4ddafa53..0967ef424b 100644 --- a/parser/testdata/03370_join_identifiers/metadata.json +++ b/parser/testdata/03370_join_identifiers/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt3": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03401_remote_bool/metadata.json b/parser/testdata/03401_remote_bool/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03401_remote_bool/metadata.json +++ b/parser/testdata/03401_remote_bool/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/03413_group_by_all_in_subquery/metadata.json b/parser/testdata/03413_group_by_all_in_subquery/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/03413_group_by_all_in_subquery/metadata.json +++ b/parser/testdata/03413_group_by_all_in_subquery/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} diff --git a/parser/testdata/03453_group_by_all_grouping/metadata.json b/parser/testdata/03453_group_by_all_grouping/metadata.json index 7da5190820..0967ef424b 100644 --- a/parser/testdata/03453_group_by_all_grouping/metadata.json +++ b/parser/testdata/03453_group_by_all_grouping/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt2":true,"stmt3":true,"stmt4":true,"stmt5":true}} +{} diff --git a/parser/testdata/03532_json_dynamic_updates/metadata.json b/parser/testdata/03532_json_dynamic_updates/metadata.json index 6a233ed346..c0f43c5c6d 100644 --- a/parser/testdata/03532_json_dynamic_updates/metadata.json +++ b/parser/testdata/03532_json_dynamic_updates/metadata.json @@ -1,14 +1,6 @@ { "explain_todo": { - "stmt12": true, - "stmt14": true, - "stmt24": true, - "stmt26": true, "stmt33": true, - "stmt39": true, - "stmt41": true, - "stmt51": true, - "stmt53": true, "stmt6": true } } diff --git a/parser/testdata/03577_temporary_table_as/metadata.json b/parser/testdata/03577_temporary_table_as/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/03577_temporary_table_as/metadata.json +++ b/parser/testdata/03577_temporary_table_as/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/03583_rewrite_in_to_join/metadata.json b/parser/testdata/03583_rewrite_in_to_join/metadata.json index b330691357..0967ef424b 100644 --- a/parser/testdata/03583_rewrite_in_to_join/metadata.json +++ b/parser/testdata/03583_rewrite_in_to_join/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt26": true - } -} +{} diff --git a/parser/testdata/03639_hash_of_json_column/metadata.json b/parser/testdata/03639_hash_of_json_column/metadata.json index f4c74e32be..0967ef424b 100644 --- a/parser/testdata/03639_hash_of_json_column/metadata.json +++ b/parser/testdata/03639_hash_of_json_column/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt10": true - } -} +{} From dbd2b5e54c333fe5915ccb5ebcf00f8ad64b26b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 15:31:30 +0000 Subject: [PATCH 10/22] Add column transformer ordering support for APPLY, EXCEPT, REPLACE - Add ColumnTransformer struct to AST to preserve transformer ordering - Add Transformers field to Asterisk and ColumnsMatcher structs - Add parseColumnsApply, parseColumnsExcept, parseColumnsReplace functions - Update explain functions to output transformers in query order - Remove inline EXCEPT handling from parseColumnsMatcher for proper infix parsing Fixes explain tests for column transformers across multiple test suites. --- ast/ast.go | 33 ++- internal/explain/expressions.go | 121 +++++++++- parser/expression.go | 224 +++++++++++++++--- .../01470_columns_transformers/metadata.json | 13 +- .../metadata.json | 12 +- .../metadata.json | 9 +- .../metadata.json | 1 - .../03152_analyzer_columns_list/metadata.json | 7 +- 8 files changed, 329 insertions(+), 91 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 6af9ce4504..fadd2f7735 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -1147,11 +1147,12 @@ const ( // Asterisk represents a *. type Asterisk struct { - Position token.Position `json:"-"` - Table string `json:"table,omitempty"` // for table.* - Except []string `json:"except,omitempty"` // for * EXCEPT (col1, col2) - Replace []*ReplaceExpr `json:"replace,omitempty"` // for * REPLACE (expr AS col) - Apply []string `json:"apply,omitempty"` // for * APPLY (func1) APPLY(func2) + Position token.Position `json:"-"` + Table string `json:"table,omitempty"` // for table.* + Except []string `json:"except,omitempty"` // for * EXCEPT (col1, col2) - deprecated, use Transformers + Replace []*ReplaceExpr `json:"replace,omitempty"` // for * REPLACE (expr AS col) - deprecated, use Transformers + Apply []string `json:"apply,omitempty"` // for * APPLY (func1) APPLY(func2) - deprecated, use Transformers + Transformers []*ColumnTransformer `json:"transformers,omitempty"` // ordered list of transformers } func (a *Asterisk) Pos() token.Position { return a.Position } @@ -1168,15 +1169,27 @@ type ReplaceExpr struct { func (r *ReplaceExpr) Pos() token.Position { return r.Position } func (r *ReplaceExpr) End() token.Position { return r.Position } +// ColumnTransformer represents a single transformer (APPLY, EXCEPT, or REPLACE) in order. +type ColumnTransformer struct { + Position token.Position `json:"-"` + Type string `json:"type"` // "apply", "except", "replace" + Apply string `json:"apply,omitempty"` // function name for APPLY + Except []string `json:"except,omitempty"` // column names for EXCEPT + Replaces []*ReplaceExpr `json:"replaces,omitempty"` // replacement expressions for REPLACE +} + // ColumnsMatcher represents COLUMNS('pattern') or COLUMNS(col1, col2) expression. // When Pattern is set, it's a regex matcher (ColumnsRegexpMatcher in explain). // When Columns is set, it's a list matcher (ColumnsListMatcher in explain). type ColumnsMatcher struct { - Position token.Position `json:"-"` - Pattern string `json:"pattern,omitempty"` - Columns []Expression `json:"columns,omitempty"` // For COLUMNS(id, name) syntax - Except []string `json:"except,omitempty"` - Qualifier string `json:"qualifier,omitempty"` // For qualified matchers like table.COLUMNS(...) + Position token.Position `json:"-"` + Pattern string `json:"pattern,omitempty"` + Columns []Expression `json:"columns,omitempty"` // For COLUMNS(id, name) syntax + Except []string `json:"except,omitempty"` // for EXCEPT (col1, col2) - deprecated, use Transformers + Replace []*ReplaceExpr `json:"replace,omitempty"` // for REPLACE (expr AS col) - deprecated, use Transformers + Apply []string `json:"apply,omitempty"` // for APPLY (func1) APPLY(func2) - deprecated, use Transformers + Qualifier string `json:"qualifier,omitempty"` // For qualified matchers like table.COLUMNS(...) + Transformers []*ColumnTransformer `json:"transformers,omitempty"` // ordered list of transformers } func (c *ColumnsMatcher) Pos() token.Position { return c.Position } diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index 3d96a1e78a..a2717b38fc 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -585,7 +585,7 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { func explainAsterisk(sb *strings.Builder, n *ast.Asterisk, indent string, depth int) { // Check if there are any column transformers (EXCEPT, REPLACE, APPLY) - hasTransformers := len(n.Except) > 0 || len(n.Replace) > 0 || len(n.Apply) > 0 + hasTransformers := len(n.Transformers) > 0 || len(n.Except) > 0 || len(n.Replace) > 0 || len(n.Apply) > 0 if n.Table != "" { if hasTransformers { @@ -607,6 +607,16 @@ func explainAsterisk(sb *strings.Builder, n *ast.Asterisk, indent string, depth } func explainColumnsTransformers(sb *strings.Builder, n *ast.Asterisk, indent string, depth int) { + // Use Transformers if available (preserves order), otherwise fall back to legacy arrays + if len(n.Transformers) > 0 { + fmt.Fprintf(sb, "%sColumnsTransformerList (children %d)\n", indent, len(n.Transformers)) + for _, t := range n.Transformers { + explainSingleTransformer(sb, t, indent, depth) + } + return + } + + // Legacy: use separate arrays (doesn't preserve order) transformerCount := 0 if len(n.Except) > 0 { transformerCount++ @@ -647,7 +657,34 @@ func explainColumnsTransformers(sb *strings.Builder, n *ast.Asterisk, indent str } } +func explainSingleTransformer(sb *strings.Builder, t *ast.ColumnTransformer, indent string, depth int) { + switch t.Type { + case "apply": + fmt.Fprintf(sb, "%s ColumnsApplyTransformer\n", indent) + case "except": + fmt.Fprintf(sb, "%s ColumnsExceptTransformer (children %d)\n", indent, len(t.Except)) + for _, col := range t.Except { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, col) + } + case "replace": + fmt.Fprintf(sb, "%s ColumnsReplaceTransformer (children %d)\n", indent, len(t.Replaces)) + for _, replace := range t.Replaces { + children := 0 + if replace.Expr != nil { + children = 1 + } + fmt.Fprintf(sb, "%s ColumnsReplaceTransformer::Replacement (children %d)\n", indent, children) + if replace.Expr != nil { + Node(sb, replace.Expr, depth+3) + } + } + } +} + func explainColumnsMatcher(sb *strings.Builder, n *ast.ColumnsMatcher, indent string, depth int) { + // Check if there are any column transformers (EXCEPT, REPLACE, APPLY) + hasTransformers := len(n.Transformers) > 0 || len(n.Except) > 0 || len(n.Replace) > 0 || len(n.Apply) > 0 + // Determine the matcher type based on whether it's a pattern or a list if len(n.Columns) > 0 { // ColumnsListMatcher for COLUMNS(col1, col2, ...) @@ -655,18 +692,25 @@ func explainColumnsMatcher(sb *strings.Builder, n *ast.ColumnsMatcher, indent st if n.Qualifier != "" { typeName = "QualifiedColumnsListMatcher" } + childCount := 1 // ExpressionList of columns + if n.Qualifier != "" { + childCount++ + } + if hasTransformers { + childCount++ // for ColumnsTransformerList + } + fmt.Fprintf(sb, "%s%s (children %d)\n", indent, typeName, childCount) if n.Qualifier != "" { - // QualifiedColumnsListMatcher has qualifier as a child - fmt.Fprintf(sb, "%s%s (children %d)\n", indent, typeName, 2) fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Qualifier) - } else { - fmt.Fprintf(sb, "%s%s (children %d)\n", indent, typeName, 1) } // Output the columns as ExpressionList fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Columns)) for _, col := range n.Columns { Node(sb, col, depth+2) } + if hasTransformers { + explainColumnsMatcherTransformers(sb, n, indent+" ", depth+1) + } } else { // ColumnsRegexpMatcher for COLUMNS('pattern') typeName := "ColumnsRegexpMatcher" @@ -674,12 +718,75 @@ func explainColumnsMatcher(sb *strings.Builder, n *ast.ColumnsMatcher, indent st typeName = "QualifiedColumnsRegexpMatcher" } if n.Qualifier != "" { - fmt.Fprintf(sb, "%s%s (children %d)\n", indent, typeName, 1) + childCount := 1 // Identifier + if hasTransformers { + childCount++ + } + fmt.Fprintf(sb, "%s%s (children %d)\n", indent, typeName, childCount) fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Qualifier) + if hasTransformers { + explainColumnsMatcherTransformers(sb, n, indent+" ", depth+1) + } } else { - fmt.Fprintf(sb, "%s%s\n", indent, typeName) + if hasTransformers { + fmt.Fprintf(sb, "%s%s (children %d)\n", indent, typeName, 1) + explainColumnsMatcherTransformers(sb, n, indent+" ", depth+1) + } else { + fmt.Fprintf(sb, "%s%s\n", indent, typeName) + } + } + } +} + +func explainColumnsMatcherTransformers(sb *strings.Builder, n *ast.ColumnsMatcher, indent string, depth int) { + // Use Transformers if available (preserves order), otherwise fall back to legacy arrays + if len(n.Transformers) > 0 { + fmt.Fprintf(sb, "%sColumnsTransformerList (children %d)\n", indent, len(n.Transformers)) + for _, t := range n.Transformers { + explainSingleTransformer(sb, t, indent, depth) + } + return + } + + // Legacy: use separate arrays (doesn't preserve order) + transformerCount := 0 + if len(n.Except) > 0 { + transformerCount++ + } + if len(n.Replace) > 0 { + transformerCount++ + } + // Each APPLY adds one transformer + transformerCount += len(n.Apply) + + fmt.Fprintf(sb, "%sColumnsTransformerList (children %d)\n", indent, transformerCount) + + if len(n.Except) > 0 { + fmt.Fprintf(sb, "%s ColumnsExceptTransformer (children %d)\n", indent, len(n.Except)) + for _, col := range n.Except { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, col) + } + } + + if len(n.Replace) > 0 { + fmt.Fprintf(sb, "%s ColumnsReplaceTransformer (children %d)\n", indent, len(n.Replace)) + for _, replace := range n.Replace { + children := 0 + if replace.Expr != nil { + children = 1 + } + fmt.Fprintf(sb, "%s ColumnsReplaceTransformer::Replacement (children %d)\n", indent, children) + if replace.Expr != nil { + // Output the expression without alias - the replacement name is implied + Node(sb, replace.Expr, depth+3) + } } } + + // Each APPLY function gets its own ColumnsApplyTransformer + for range n.Apply { + fmt.Fprintf(sb, "%s ColumnsApplyTransformer\n", indent) + } } func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string, depth int) { diff --git a/parser/expression.go b/parser/expression.go index 1d6b07e1f7..9ef6eb6112 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -407,22 +407,31 @@ func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { case token.ARROW: return p.parseLambda(left) case token.EXCEPT: - // Handle * EXCEPT (col1, col2) + // Handle * EXCEPT (col1, col2) or COLUMNS(...) EXCEPT (col1, col2) if asterisk, ok := left.(*ast.Asterisk); ok { return p.parseAsteriskExcept(asterisk) } + if matcher, ok := left.(*ast.ColumnsMatcher); ok { + return p.parseColumnsExcept(matcher) + } return left case token.REPLACE: - // Handle * REPLACE (expr AS col) + // Handle * REPLACE (expr AS col) or COLUMNS(...) REPLACE (expr AS col) if asterisk, ok := left.(*ast.Asterisk); ok { return p.parseAsteriskReplace(asterisk) } + if matcher, ok := left.(*ast.ColumnsMatcher); ok { + return p.parseColumnsReplace(matcher) + } return left case token.APPLY: - // Handle * APPLY (func) or * APPLY func + // Handle * APPLY (func) or COLUMNS(...) APPLY(func) if asterisk, ok := left.(*ast.Asterisk); ok { return p.parseAsteriskApply(asterisk) } + if matcher, ok := left.(*ast.ColumnsMatcher); ok { + return p.parseColumnsApply(matcher) + } return left case token.NUMBER: // Handle tuple access like t.1 where .1 is lexed as a number @@ -2208,22 +2217,8 @@ func (p *Parser) parseColumnsMatcher() ast.Expression { p.expect(token.RPAREN) - // Handle EXCEPT - if p.currentIs(token.EXCEPT) { - p.nextToken() - if p.expect(token.LPAREN) { - for !p.currentIs(token.RPAREN) && !p.currentIs(token.EOF) { - if p.currentIs(token.IDENT) { - matcher.Except = append(matcher.Except, p.current.Value) - p.nextToken() - } - if p.currentIs(token.COMMA) { - p.nextToken() - } - } - p.expect(token.RPAREN) - } - } + // EXCEPT, REPLACE, and APPLY are now handled via infix parsing + // to preserve transformer ordering return matcher } @@ -2260,22 +2255,8 @@ func (p *Parser) parseQualifiedColumnsMatcher(qualifier string, pos token.Positi p.expect(token.RPAREN) - // Handle EXCEPT - if p.currentIs(token.EXCEPT) { - p.nextToken() - if p.expect(token.LPAREN) { - for !p.currentIs(token.RPAREN) && !p.currentIs(token.EOF) { - if p.currentIs(token.IDENT) { - matcher.Except = append(matcher.Except, p.current.Value) - p.nextToken() - } - if p.currentIs(token.COMMA) { - p.nextToken() - } - } - p.expect(token.RPAREN) - } - } + // EXCEPT, REPLACE, and APPLY are now handled via infix parsing + // to preserve transformer ordering return matcher } @@ -2401,6 +2382,7 @@ func (p *Parser) parseKeywordAsIdentifier() ast.Expression { } func (p *Parser) parseAsteriskExcept(asterisk *ast.Asterisk) ast.Expression { + pos := p.current.Pos p.nextToken() // skip EXCEPT // EXCEPT can have optional parentheses: * EXCEPT (col1, col2) or * EXCEPT col @@ -2409,9 +2391,11 @@ func (p *Parser) parseAsteriskExcept(asterisk *ast.Asterisk) ast.Expression { p.nextToken() // skip ( } + var exceptCols []string // Parse column names (can be IDENT or keywords) for { if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + exceptCols = append(exceptCols, p.current.Value) asterisk.Except = append(asterisk.Except, p.current.Value) p.nextToken() } @@ -2423,6 +2407,14 @@ func (p *Parser) parseAsteriskExcept(asterisk *ast.Asterisk) ast.Expression { } } + if len(exceptCols) > 0 { + asterisk.Transformers = append(asterisk.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "except", + Except: exceptCols, + }) + } + if hasParens { p.expect(token.RPAREN) } @@ -2431,6 +2423,7 @@ func (p *Parser) parseAsteriskExcept(asterisk *ast.Asterisk) ast.Expression { } func (p *Parser) parseAsteriskReplace(asterisk *ast.Asterisk) ast.Expression { + pos := p.current.Pos p.nextToken() // skip REPLACE // REPLACE can have optional parentheses: REPLACE (expr AS col) or REPLACE expr AS col @@ -2439,6 +2432,7 @@ func (p *Parser) parseAsteriskReplace(asterisk *ast.Asterisk) ast.Expression { p.nextToken() } + var replaces []*ast.ReplaceExpr for { // Stop conditions based on context if hasParens && p.currentIs(token.RPAREN) { @@ -2466,6 +2460,7 @@ func (p *Parser) parseAsteriskReplace(asterisk *ast.Asterisk) ast.Expression { } asterisk.Replace = append(asterisk.Replace, replace) + replaces = append(replaces, replace) if p.currentIs(token.COMMA) { p.nextToken() @@ -2478,6 +2473,14 @@ func (p *Parser) parseAsteriskReplace(asterisk *ast.Asterisk) ast.Expression { } } + if len(replaces) > 0 { + asterisk.Transformers = append(asterisk.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "replace", + Replaces: replaces, + }) + } + if hasParens { p.expect(token.RPAREN) } @@ -2486,6 +2489,7 @@ func (p *Parser) parseAsteriskReplace(asterisk *ast.Asterisk) ast.Expression { } func (p *Parser) parseAsteriskApply(asterisk *ast.Asterisk) ast.Expression { + pos := p.current.Pos p.nextToken() // skip APPLY // APPLY can have optional parentheses: * APPLY(func) or * APPLY func @@ -2496,7 +2500,13 @@ func (p *Parser) parseAsteriskApply(asterisk *ast.Asterisk) ast.Expression { // Parse function name (can be IDENT or keyword like sum, avg, etc.) if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { - asterisk.Apply = append(asterisk.Apply, p.current.Value) + funcName := p.current.Value + asterisk.Apply = append(asterisk.Apply, funcName) + asterisk.Transformers = append(asterisk.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "apply", + Apply: funcName, + }) p.nextToken() } @@ -2506,3 +2516,145 @@ func (p *Parser) parseAsteriskApply(asterisk *ast.Asterisk) ast.Expression { return asterisk } + +func (p *Parser) parseColumnsApply(matcher *ast.ColumnsMatcher) ast.Expression { + pos := p.current.Pos + p.nextToken() // skip APPLY + + // APPLY can have optional parentheses: COLUMNS(...) APPLY(func) or COLUMNS(...) APPLY func + hasParens := p.currentIs(token.LPAREN) + if hasParens { + p.nextToken() // skip ( + } + + // Parse function name (can be IDENT or keyword like sum, avg, etc.) + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + funcName := p.current.Value + matcher.Apply = append(matcher.Apply, funcName) + matcher.Transformers = append(matcher.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "apply", + Apply: funcName, + }) + p.nextToken() + } + + if hasParens { + p.expect(token.RPAREN) + } + + return matcher +} + +func (p *Parser) parseColumnsExcept(matcher *ast.ColumnsMatcher) ast.Expression { + pos := p.current.Pos + p.nextToken() // skip EXCEPT + + // EXCEPT can have optional parentheses: COLUMNS(...) EXCEPT (col1, col2) or COLUMNS(...) EXCEPT col + hasParens := p.currentIs(token.LPAREN) + if hasParens { + p.nextToken() // skip ( + } + + var exceptCols []string + // Parse column names (can be IDENT or keywords) + for { + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + exceptCols = append(exceptCols, p.current.Value) + matcher.Except = append(matcher.Except, p.current.Value) + p.nextToken() + } + + if hasParens && p.currentIs(token.COMMA) { + p.nextToken() + } else { + break + } + } + + if len(exceptCols) > 0 { + matcher.Transformers = append(matcher.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "except", + Except: exceptCols, + }) + } + + if hasParens { + p.expect(token.RPAREN) + } + + return matcher +} + +func (p *Parser) parseColumnsReplace(matcher *ast.ColumnsMatcher) ast.Expression { + pos := p.current.Pos + p.nextToken() // skip REPLACE + + // Check for STRICT modifier + if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "STRICT" { + p.nextToken() + } + + // REPLACE can have optional parentheses: REPLACE (expr AS col) or REPLACE expr AS col + hasParens := p.currentIs(token.LPAREN) + if hasParens { + p.nextToken() + } + + var replaces []*ast.ReplaceExpr + for { + // Stop conditions based on context + if hasParens && p.currentIs(token.RPAREN) { + break + } + if !hasParens && (p.currentIs(token.FROM) || p.currentIs(token.WHERE) || p.currentIs(token.EOF) || + p.currentIs(token.GROUP) || p.currentIs(token.ORDER) || p.currentIs(token.HAVING) || + p.currentIs(token.LIMIT) || p.currentIs(token.SETTINGS) || p.currentIs(token.FORMAT) || + p.currentIs(token.UNION) || p.currentIs(token.EXCEPT) || p.currentIs(token.COMMA) || + p.currentIs(token.APPLY)) { + break + } + + replace := &ast.ReplaceExpr{ + Position: p.current.Pos, + } + + replace.Expr = p.parseExpression(ALIAS_PREC) + + if p.currentIs(token.AS) { + p.nextToken() + if p.currentIs(token.IDENT) { + replace.Name = p.current.Value + p.nextToken() + } + } + + matcher.Replace = append(matcher.Replace, replace) + replaces = append(replaces, replace) + + if p.currentIs(token.COMMA) { + p.nextToken() + // If no parens and we see comma, might be end of select column + if !hasParens { + break + } + } else if !hasParens { + break + } + } + + if len(replaces) > 0 { + matcher.Transformers = append(matcher.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "replace", + Replaces: replaces, + }) + } + + if hasParens { + p.expect(token.RPAREN) + } + + return matcher +} diff --git a/parser/testdata/01470_columns_transformers/metadata.json b/parser/testdata/01470_columns_transformers/metadata.json index 2e74062554..34f8786c4b 100644 --- a/parser/testdata/01470_columns_transformers/metadata.json +++ b/parser/testdata/01470_columns_transformers/metadata.json @@ -1,24 +1,13 @@ { "explain_todo": { - "stmt11": true, "stmt12": true, "stmt13": true, "stmt14": true, "stmt15": true, "stmt16": true, "stmt17": true, - "stmt21": true, "stmt22": true, - "stmt26": true, - "stmt29": true, - "stmt35": true, - "stmt36": true, - "stmt37": true, - "stmt38": true, - "stmt39": true, - "stmt40": true, "stmt41": true, - "stmt42": true, - "stmt8": true + "stmt42": true } } diff --git a/parser/testdata/02339_analyzer_matcher_basic/metadata.json b/parser/testdata/02339_analyzer_matcher_basic/metadata.json index 2148fda4c3..9372bbb5fc 100644 --- a/parser/testdata/02339_analyzer_matcher_basic/metadata.json +++ b/parser/testdata/02339_analyzer_matcher_basic/metadata.json @@ -1,20 +1,10 @@ { "explain_todo": { - "stmt105": true, - "stmt106": true, "stmt63": true, "stmt64": true, "stmt66": true, "stmt67": true, "stmt69": true, - "stmt70": true, - "stmt79": true, - "stmt80": true, - "stmt85": true, - "stmt86": true, - "stmt92": true, - "stmt93": true, - "stmt98": true, - "stmt99": true + "stmt70": true } } diff --git a/parser/testdata/02342_analyzer_compound_types/metadata.json b/parser/testdata/02342_analyzer_compound_types/metadata.json index a7826c148e..0967ef424b 100644 --- a/parser/testdata/02342_analyzer_compound_types/metadata.json +++ b/parser/testdata/02342_analyzer_compound_types/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/03101_analyzer_identifiers_4/metadata.json b/parser/testdata/03101_analyzer_identifiers_4/metadata.json index ca345578d8..16cecbd2f7 100644 --- a/parser/testdata/03101_analyzer_identifiers_4/metadata.json +++ b/parser/testdata/03101_analyzer_identifiers_4/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt14": true, - "stmt18": true, "stmt7": true, "stmt9": true } diff --git a/parser/testdata/03152_analyzer_columns_list/metadata.json b/parser/testdata/03152_analyzer_columns_list/metadata.json index 7b4ddafa53..0967ef424b 100644 --- a/parser/testdata/03152_analyzer_columns_list/metadata.json +++ b/parser/testdata/03152_analyzer_columns_list/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt3": true, - "stmt4": true - } -} +{} From 8fce7a94153f98b6a975ef24b1c068bdc33d2446 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 15:59:41 +0000 Subject: [PATCH 11/22] Add support for CREATE QUOTA, SET DEFAULT ROLE, and MODIFY COLUMN REMOVE COMMENT Parser changes: - Add parseCreateQuota function returning CreateQuotaQuery - Add parseSetRole function for SET DEFAULT ROLE statements - Handle MODIFY COLUMN ... REMOVE COMMENT syntax - Add FORMAT handling for SHOW GRANTS, SHOW CREATE ROLE/USER/POLICY/QUOTA/SETTINGS PROFILE AST changes: - Add CreateQuotaQuery struct - Add SetRoleQuery struct - Add Format field to ShowGrantsQuery, ShowCreateRoleQuery, ShowCreateRowPolicyQuery, ShowCreateQuotaQuery, ShowCreateSettingsProfileQuery Explain changes: - Add handlers for new statement types - Fix ColumnDeclaration to omit (children 0) when no children - Fix ALTER command type mapping (CLEAR_COLUMN -> DROP_COLUMN, DELETE_WHERE -> DELETE) - Fix TRUNCATE/CHECK queries to output db/table as separate identifiers - Add FORMAT handling for DESCRIBE, SHOW queries Fixes all 20 statements in 01702_system_query_log and many others. --- ast/ast.go | 54 +++++-- internal/explain/explain.go | 50 +++++- internal/explain/statements.go | 145 ++++++++++++++---- parser/parser.go | 140 +++++++++++++++-- .../00077_log_tinylog_stripelog/metadata.json | 11 +- parser/testdata/00716_allow_ddl/metadata.json | 6 +- .../metadata.json | 10 +- .../metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 9 +- .../testdata/01292_create_user/metadata.json | 5 - .../01349_mutation_datetime_key/metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 5 +- .../01388_clear_all_columns/metadata.json | 6 - .../metadata.json | 8 +- .../metadata.json | 8 +- .../metadata.json | 4 - .../metadata.json | 6 +- .../metadata.json | 6 +- .../01550_mutation_subquery/metadata.json | 6 +- .../01603_remove_column_ttl/metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 6 +- .../01702_system_query_log/metadata.json | 25 +-- .../01745_alter_delete_view/metadata.json | 6 +- .../metadata.json | 6 +- .../01821_join_table_mutation/metadata.json | 9 +- .../metadata.json | 6 +- .../metadata.json | 6 +- .../testdata/01947_mv_subquery/metadata.json | 7 +- .../02008_materialize_column/metadata.json | 1 - .../metadata.json | 1 - .../metadata.json | 6 +- .../metadata.json | 1 - .../metadata.json | 1 - .../02352_lightweight_delete/metadata.json | 1 - .../02416_rocksdb_delete_update/metadata.json | 3 +- .../metadata.json | 1 - .../metadata.json | 1 - .../metadata.json | 1 - .../metadata.json | 3 +- .../metadata.json | 1 - .../metadata.json | 6 +- .../metadata.json | 1 - .../metadata.json | 3 +- .../metadata.json | 7 +- .../metadata.json | 1 - .../02842_truncate_database/metadata.json | 4 +- .../metadata.json | 6 +- .../02870_per_column_settings/metadata.json | 1 - .../metadata.json | 6 +- .../02932_lwd_and_mutations/metadata.json | 6 +- .../metadata.json | 1 - .../metadata.json | 1 - .../03047_on_fly_update_delete/metadata.json | 1 - .../metadata.json | 1 - .../03100_lwu_32_on_fly_filter/metadata.json | 2 - .../03100_lwu_deletes_2/metadata.json | 1 - .../metadata.json | 6 +- .../metadata.json | 1 - .../metadata.json | 6 +- .../metadata.json | 1 - .../03275_block_number_mutation/metadata.json | 7 +- .../metadata.json | 6 +- .../03325_alter_ast_format/metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 1 - .../03636_storage_alias_basic/metadata.json | 3 +- .../metadata.json | 1 - .../metadata.json | 7 +- .../metadata.json | 6 +- .../metadata.json | 13 +- .../metadata.json | 1 - .../metadata.json | 1 - .../metadata.json | 6 +- 78 files changed, 374 insertions(+), 371 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index fadd2f7735..5cd10b2f48 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -693,18 +693,24 @@ func (s *ShowQuery) statementNode() {} type ShowType string const ( - ShowTables ShowType = "TABLES" - ShowDatabases ShowType = "DATABASES" - ShowProcesses ShowType = "PROCESSLIST" - ShowCreate ShowType = "CREATE" - ShowCreateDB ShowType = "CREATE_DATABASE" - ShowCreateDictionary ShowType = "CREATE_DICTIONARY" - ShowCreateView ShowType = "CREATE_VIEW" - ShowCreateUser ShowType = "CREATE_USER" - 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" + ShowCreateUser ShowType = "CREATE_USER" + ShowCreateRole ShowType = "CREATE_ROLE" + ShowCreatePolicy ShowType = "CREATE_POLICY" + ShowCreateRowPolicy ShowType = "CREATE_ROW_POLICY" + ShowCreateQuota ShowType = "CREATE_QUOTA" + ShowCreateSettingsProfile ShowType = "CREATE_SETTINGS_PROFILE" + ShowColumns ShowType = "COLUMNS" + ShowDictionaries ShowType = "DICTIONARIES" + ShowFunctions ShowType = "FUNCTIONS" + ShowSettings ShowType = "SETTINGS" + ShowGrants ShowType = "GRANTS" ) // ExplainQuery represents an EXPLAIN statement. @@ -866,6 +872,7 @@ func (g *GrantQuery) statementNode() {} // ShowGrantsQuery represents a SHOW GRANTS statement. type ShowGrantsQuery struct { Position token.Position `json:"-"` + Format string `json:"format,omitempty"` } func (s *ShowGrantsQuery) Pos() token.Position { return s.Position } @@ -885,12 +892,23 @@ func (s *ShowPrivilegesQuery) statementNode() {} type ShowCreateQuotaQuery struct { Position token.Position `json:"-"` Name string `json:"name,omitempty"` + Format string `json:"format,omitempty"` } func (s *ShowCreateQuotaQuery) Pos() token.Position { return s.Position } func (s *ShowCreateQuotaQuery) End() token.Position { return s.Position } func (s *ShowCreateQuotaQuery) statementNode() {} +// CreateQuotaQuery represents a CREATE QUOTA statement. +type CreateQuotaQuery struct { + Position token.Position `json:"-"` + Name string `json:"name,omitempty"` +} + +func (c *CreateQuotaQuery) Pos() token.Position { return c.Position } +func (c *CreateQuotaQuery) End() token.Position { return c.Position } +func (c *CreateQuotaQuery) statementNode() {} + // CreateSettingsProfileQuery represents a CREATE SETTINGS PROFILE statement. type CreateSettingsProfileQuery struct { Position token.Position `json:"-"` @@ -926,6 +944,7 @@ func (d *DropSettingsProfileQuery) statementNode() {} type ShowCreateSettingsProfileQuery struct { Position token.Position `json:"-"` Names []string `json:"names,omitempty"` + Format string `json:"format,omitempty"` } func (s *ShowCreateSettingsProfileQuery) Pos() token.Position { return s.Position } @@ -955,6 +974,7 @@ func (d *DropRowPolicyQuery) statementNode() {} // ShowCreateRowPolicyQuery represents a SHOW CREATE ROW POLICY statement. type ShowCreateRowPolicyQuery struct { Position token.Position `json:"-"` + Format string `json:"format,omitempty"` } func (s *ShowCreateRowPolicyQuery) Pos() token.Position { return s.Position } @@ -985,12 +1005,22 @@ func (d *DropRoleQuery) statementNode() {} type ShowCreateRoleQuery struct { Position token.Position `json:"-"` RoleCount int `json:"role_count,omitempty"` // Number of roles specified + Format string `json:"format,omitempty"` } func (s *ShowCreateRoleQuery) Pos() token.Position { return s.Position } func (s *ShowCreateRoleQuery) End() token.Position { return s.Position } func (s *ShowCreateRoleQuery) statementNode() {} +// SetRoleQuery represents a SET DEFAULT ROLE statement. +type SetRoleQuery struct { + Position token.Position `json:"-"` +} + +func (s *SetRoleQuery) Pos() token.Position { return s.Position } +func (s *SetRoleQuery) End() token.Position { return s.Position } +func (s *SetRoleQuery) statementNode() {} + // CreateResourceQuery represents a CREATE RESOURCE statement. type CreateResourceQuery struct { Position token.Position `json:"-"` diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 9cf2b8414d..1c052a68d6 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -119,6 +119,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) { explainExchangeQuery(sb, n, indent) case *ast.SetQuery: explainSetQuery(sb, indent) + case *ast.SetRoleQuery: + fmt.Fprintf(sb, "%sSetRoleQuery\n", indent) case *ast.SystemQuery: explainSystemQuery(sb, n, indent) case *ast.TransactionControlQuery: @@ -130,7 +132,14 @@ func Node(sb *strings.Builder, node interface{}, depth int) { case *ast.ShowPrivilegesQuery: fmt.Fprintf(sb, "%sShowPrivilegesQuery\n", indent) case *ast.ShowCreateQuotaQuery: - fmt.Fprintf(sb, "%sSHOW CREATE QUOTA query\n", indent) + if n.Format != "" { + fmt.Fprintf(sb, "%sSHOW CREATE QUOTA query (children 1)\n", indent) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } else { + fmt.Fprintf(sb, "%sSHOW CREATE QUOTA query\n", indent) + } + case *ast.CreateQuotaQuery: + fmt.Fprintf(sb, "%sCreateQuotaQuery\n", indent) case *ast.CreateSettingsProfileQuery: fmt.Fprintf(sb, "%sCreateSettingsProfileQuery\n", indent) case *ast.AlterSettingsProfileQuery: @@ -140,27 +149,43 @@ func Node(sb *strings.Builder, node interface{}, depth int) { fmt.Fprintf(sb, "%sDROP SETTINGS PROFILE query\n", indent) case *ast.ShowCreateSettingsProfileQuery: // Use PROFILES (plural) when multiple profiles are specified + queryName := "SHOW CREATE SETTINGS PROFILE query" if len(n.Names) > 1 { - fmt.Fprintf(sb, "%sSHOW CREATE SETTINGS PROFILES query\n", indent) + queryName = "SHOW CREATE SETTINGS PROFILES query" + } + if n.Format != "" { + fmt.Fprintf(sb, "%s%s (children 1)\n", indent, queryName) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) } else { - fmt.Fprintf(sb, "%sSHOW CREATE SETTINGS PROFILE query\n", indent) + fmt.Fprintf(sb, "%s%s\n", indent, queryName) } case *ast.CreateRowPolicyQuery: fmt.Fprintf(sb, "%sCREATE ROW POLICY or ALTER ROW POLICY query\n", indent) case *ast.DropRowPolicyQuery: fmt.Fprintf(sb, "%sDROP ROW POLICY query\n", indent) case *ast.ShowCreateRowPolicyQuery: - fmt.Fprintf(sb, "%sSHOW CREATE ROW POLICY query\n", indent) + // ClickHouse uses "ROW POLICIES" (plural) when FORMAT is present + if n.Format != "" { + fmt.Fprintf(sb, "%sSHOW CREATE ROW POLICIES query (children 1)\n", indent) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } else { + fmt.Fprintf(sb, "%sSHOW CREATE ROW POLICY query\n", indent) + } case *ast.CreateRoleQuery: fmt.Fprintf(sb, "%sCreateRoleQuery\n", indent) case *ast.DropRoleQuery: fmt.Fprintf(sb, "%sDROP ROLE query\n", indent) case *ast.ShowCreateRoleQuery: // Use ROLES (plural) when multiple roles are specified + queryName := "SHOW CREATE ROLE query" if n.RoleCount > 1 { - fmt.Fprintf(sb, "%sSHOW CREATE ROLES query\n", indent) + queryName = "SHOW CREATE ROLES query" + } + if n.Format != "" { + fmt.Fprintf(sb, "%s%s (children 1)\n", indent, queryName) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) } else { - fmt.Fprintf(sb, "%sSHOW CREATE ROLE query\n", indent) + fmt.Fprintf(sb, "%s%s\n", indent, queryName) } case *ast.CreateResourceQuery: fmt.Fprintf(sb, "%sCreateResourceQuery %s (children 1)\n", indent, n.Name) @@ -181,7 +206,12 @@ func Node(sb *strings.Builder, node interface{}, depth int) { case *ast.DropWorkloadQuery: fmt.Fprintf(sb, "%sDropWorkloadQuery\n", indent) case *ast.ShowGrantsQuery: - fmt.Fprintf(sb, "%sShowGrantsQuery\n", indent) + if n.Format != "" { + fmt.Fprintf(sb, "%sShowGrantsQuery (children 1)\n", indent) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } else { + fmt.Fprintf(sb, "%sShowGrantsQuery\n", indent) + } case *ast.GrantQuery: fmt.Fprintf(sb, "%sGrantQuery\n", indent) case *ast.UseQuery: @@ -285,7 +315,11 @@ func Column(sb *strings.Builder, col *ast.ColumnDeclaration, depth int) { if col.Codec != nil { children++ } - fmt.Fprintf(sb, "%sColumnDeclaration %s (children %d)\n", indent, col.Name, children) + if children > 0 { + fmt.Fprintf(sb, "%sColumnDeclaration %s (children %d)\n", indent, col.Name, children) + } else { + fmt.Fprintf(sb, "%sColumnDeclaration %s\n", indent, col.Name) + } if col.Type != nil { Node(sb, col.Type, depth+1) } diff --git a/internal/explain/statements.go b/internal/explain/statements.go index e9a5eb6561..87182883f2 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -672,32 +672,50 @@ func explainShowQuery(sb *strings.Builder, n *ast.ShowQuery, indent string) { if n.ShowType == ast.ShowCreateDictionary && (n.Database != "" || n.From != "") { if n.Database != "" && n.From != "" { children := 2 + if n.Format != "" { + children++ + } if n.HasSettings { children++ } fmt.Fprintf(sb, "%sShowCreateDictionaryQuery %s %s (children %d)\n", indent, n.Database, n.From, children) fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } if n.HasSettings { fmt.Fprintf(sb, "%s Set\n", indent) } } else if n.From != "" { children := 1 + if n.Format != "" { + children++ + } if n.HasSettings { children++ } fmt.Fprintf(sb, "%sShowCreateDictionaryQuery %s (children %d)\n", indent, n.From, children) fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } if n.HasSettings { fmt.Fprintf(sb, "%s Set\n", indent) } } else if n.Database != "" { children := 1 + if n.Format != "" { + children++ + } if n.HasSettings { children++ } fmt.Fprintf(sb, "%sShowCreateDictionaryQuery %s (children %d)\n", indent, n.Database, children) fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } if n.HasSettings { fmt.Fprintf(sb, "%s Set\n", indent) } @@ -803,14 +821,41 @@ func explainShowQuery(sb *strings.Builder, n *ast.ShowQuery, indent string) { // SHOW CREATE USER has special output format if n.ShowType == ast.ShowCreateUser { - fmt.Fprintf(sb, "%sSHOW CREATE USER query\n", indent) + if n.Format != "" { + fmt.Fprintf(sb, "%sSHOW CREATE USER query (children 1)\n", indent) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } else { + fmt.Fprintf(sb, "%sSHOW CREATE USER query\n", indent) + } return } - // SHOW TABLES FROM database - include database as child - if n.ShowType == ast.ShowTables && n.From != "" { - fmt.Fprintf(sb, "%sShowTables (children 1)\n", indent) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + // SHOW TABLES/DATABASES/DICTIONARIES - include FROM and FORMAT as children + if n.ShowType == ast.ShowTables || n.ShowType == ast.ShowDatabases || n.ShowType == ast.ShowDictionaries { + children := 0 + if n.From != "" { + children++ + } + if n.Format != "" { + children++ + } + if n.HasSettings { + children++ + } + if children > 0 { + fmt.Fprintf(sb, "%sShowTables (children %d)\n", indent, children) + if n.From != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + } + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } + if n.HasSettings { + fmt.Fprintf(sb, "%s Set\n", indent) + } + } else { + fmt.Fprintf(sb, "%sShowTables\n", indent) + } return } @@ -826,23 +871,35 @@ func explainDescribeQuery(sb *strings.Builder, n *ast.DescribeQuery, indent stri if n.TableExpr != nil { // DESCRIBE on a subquery - TableExpr contains a TableExpression with a Subquery children := 1 + if n.Format != "" { + children++ + } if len(n.Settings) > 0 { children++ } fmt.Fprintf(sb, "%sDescribeQuery (children %d)\n", indent, children) Node(sb, n.TableExpr, depth+1) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } if len(n.Settings) > 0 { fmt.Fprintf(sb, "%s Set\n", indent) } } else if n.TableFunction != nil { // DESCRIBE on a table function - wrap in TableExpression children := 1 + if n.Format != "" { + children++ + } if len(n.Settings) > 0 { children++ } fmt.Fprintf(sb, "%sDescribeQuery (children %d)\n", indent, children) fmt.Fprintf(sb, "%s TableExpression (children 1)\n", indent) explainFunctionCall(sb, n.TableFunction, indent+" ", 2) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } if len(n.Settings) > 0 { fmt.Fprintf(sb, "%s Set\n", indent) } @@ -853,12 +910,18 @@ func explainDescribeQuery(sb *strings.Builder, n *ast.DescribeQuery, indent stri name = n.Database + "." + n.Table } children := 1 + if n.Format != "" { + children++ + } if len(n.Settings) > 0 { children++ } fmt.Fprintf(sb, "%sDescribeQuery (children %d)\n", indent, children) fmt.Fprintf(sb, "%s TableExpression (children 1)\n", indent) fmt.Fprintf(sb, "%s TableIdentifier %s\n", indent, name) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } if len(n.Settings) > 0 { fmt.Fprintf(sb, "%s Set\n", indent) } @@ -1027,6 +1090,14 @@ func explainAlterCommand(sb *strings.Builder, cmd *ast.AlterCommand, indent stri if cmdType == ast.AlterDetachPartition { cmdType = ast.AlterDropPartition } + // CLEAR_COLUMN is shown as DROP_COLUMN in EXPLAIN AST + if cmdType == ast.AlterClearColumn { + cmdType = ast.AlterDropColumn + } + // DELETE_WHERE is shown as DELETE in EXPLAIN AST + if cmdType == ast.AlterDeleteWhere { + cmdType = "DELETE" + } fmt.Fprintf(sb, "%sAlterCommand %s (children %d)\n", indent, cmdType, children) switch cmd.Type { @@ -1360,13 +1431,15 @@ func explainTruncateQuery(sb *strings.Builder, n *ast.TruncateQuery, indent stri return } - name := n.Table if n.Database != "" { - name = n.Database + "." + n.Table + // Database-qualified: TruncateQuery db table (children 2) + fmt.Fprintf(sb, "%sTruncateQuery %s %s (children 2)\n", indent, n.Database, n.Table) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) + } else { + fmt.Fprintf(sb, "%sTruncateQuery %s (children 1)\n", indent, n.Table) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) } - - fmt.Fprintf(sb, "%sTruncateQuery %s (children %d)\n", indent, name, 1) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) } func explainCheckQuery(sb *strings.Builder, n *ast.CheckQuery, indent string) { @@ -1375,26 +1448,40 @@ func explainCheckQuery(sb *strings.Builder, n *ast.CheckQuery, indent string) { return } - name := n.Table if n.Database != "" { - name = n.Database + "." + n.Table - } - - children := 1 // identifier - if n.Format != "" { - children++ - } - if len(n.Settings) > 0 { - children++ - } - - fmt.Fprintf(sb, "%sCheckQuery %s (children %d)\n", indent, name, children) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) - if n.Format != "" { - fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) - } - if len(n.Settings) > 0 { - fmt.Fprintf(sb, "%s Set\n", indent) + // Database-qualified: CheckQuery db table (children N) + children := 2 // database + table identifiers + if n.Format != "" { + children++ + } + if len(n.Settings) > 0 { + children++ + } + fmt.Fprintf(sb, "%sCheckQuery %s %s (children %d)\n", indent, n.Database, n.Table, children) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } + if len(n.Settings) > 0 { + fmt.Fprintf(sb, "%s Set\n", indent) + } + } else { + children := 1 // table identifier + if n.Format != "" { + children++ + } + if len(n.Settings) > 0 { + children++ + } + fmt.Fprintf(sb, "%sCheckQuery %s (children %d)\n", indent, n.Table, children) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) + if n.Format != "" { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } + if len(n.Settings) > 0 { + fmt.Fprintf(sb, "%s Set\n", indent) + } } } diff --git a/parser/parser.go b/parser/parser.go index 2a45bdbb80..7bbc4530a6 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -193,6 +193,10 @@ func (p *Parser) parseStatement() ast.Statement { if p.peekIs(token.TRANSACTION) { return p.parseTransactionControl() } + // Check for SET DEFAULT ROLE + if p.peekIs(token.DEFAULT) { + return p.parseSetRole() + } return p.parseSet() case token.OPTIMIZE: return p.parseOptimize() @@ -1550,8 +1554,8 @@ func (p *Parser) parseCreate() ast.Statement { // CREATE WORKLOAD return p.parseCreateWorkload(pos) case "QUOTA": - // Skip these statements - just consume tokens until semicolon - p.parseCreateGeneric(create) + // CREATE QUOTA + return p.parseCreateQuota(pos) default: p.errors = append(p.errors, fmt.Errorf("expected TABLE, DATABASE, VIEW, FUNCTION, USER after CREATE")) return nil @@ -2358,11 +2362,20 @@ func (p *Parser) parseShowCreateSettingsProfile(pos token.Position) *ast.ShowCre break } - // Skip the rest of the statement - for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + // Skip tokens until FORMAT or end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.FORMAT) { p.nextToken() } + // Parse FORMAT clause if present + if p.currentIs(token.FORMAT) { + p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + query.Format = p.current.Value + p.nextToken() + } + } + return query } @@ -2464,9 +2477,18 @@ func (p *Parser) parseShowCreateRowPolicy(pos token.Position) *ast.ShowCreateRow p.nextToken() } - // Skip the rest of the statement (policy names, ON table, etc.) - for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + // Skip tokens until FORMAT or end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.FORMAT) { + p.nextToken() + } + + // Parse FORMAT clause if present + if p.currentIs(token.FORMAT) { p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + query.Format = p.current.Value + p.nextToken() + } } return query @@ -2540,6 +2562,30 @@ func (p *Parser) parseAlterRole() *ast.CreateRoleQuery { return query } +func (p *Parser) parseCreateQuota(pos token.Position) *ast.CreateQuotaQuery { + query := &ast.CreateQuotaQuery{ + Position: pos, + } + + // Skip QUOTA keyword + if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "QUOTA" { + p.nextToken() + } + + // Parse quota name + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + query.Name = p.current.Value + p.nextToken() + } + + // Skip the rest of the statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + p.nextToken() + } + + return query +} + func (p *Parser) parseShowCreateRole(pos token.Position) *ast.ShowCreateRoleQuery { query := &ast.ShowCreateRoleQuery{ Position: pos, @@ -2577,11 +2623,20 @@ func (p *Parser) parseShowCreateRole(pos token.Position) *ast.ShowCreateRoleQuer } } - // Skip the rest of the statement - for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + // Skip tokens until FORMAT or end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.FORMAT) { p.nextToken() } + // Parse FORMAT clause if present + if p.currentIs(token.FORMAT) { + p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + query.Format = p.current.Value + p.nextToken() + } + } + return query } @@ -4169,7 +4224,20 @@ func (p *Parser) parseAlterCommand() *ast.AlterCommand { if p.currentIs(token.COLUMN) { cmd.Type = ast.AlterModifyColumn p.nextToken() - cmd.Column = p.parseColumnDeclaration() + // Handle MODIFY COLUMN name REMOVE ... (e.g., REMOVE COMMENT) + // Check if the next token after column name is REMOVE + if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peek.Token == token.IDENT && strings.ToUpper(p.peek.Value) == "REMOVE" { + // Just parse column name without type + colName := p.current.Value + p.nextToken() // skip column name + cmd.Column = &ast.ColumnDeclaration{Name: colName} + // Skip REMOVE COMMENT etc. + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.COMMA) { + p.nextToken() + } + } else { + cmd.Column = p.parseColumnDeclaration() + } } else if p.currentIs(token.TTL) { cmd.Type = ast.AlterModifyTTL p.nextToken() @@ -4451,11 +4519,20 @@ func (p *Parser) parseShow() ast.Statement { // 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) { + query := &ast.ShowGrantsQuery{Position: pos} + // Skip tokens until FORMAT or end of statement + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.FORMAT) { + p.nextToken() + } + // Parse FORMAT clause if present + if p.currentIs(token.FORMAT) { p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + query.Format = p.current.Value + p.nextToken() + } } - return &ast.ShowGrantsQuery{Position: pos} + return query } show := &ast.ShowQuery{ @@ -4480,12 +4557,20 @@ func (p *Parser) parseShow() ast.Statement { } else if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "QUOTA" { // SHOW CREATE QUOTA p.nextToken() - name := "" + query := &ast.ShowCreateQuotaQuery{Position: pos} if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { - name = p.current.Value + query.Name = p.current.Value p.nextToken() } - return &ast.ShowCreateQuotaQuery{Position: pos, Name: name} + // Parse FORMAT clause if present + if p.currentIs(token.FORMAT) { + p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + query.Format = p.current.Value + p.nextToken() + } + } + return query } else if p.currentIs(token.SETTINGS) || (p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "PROFILE") { // SHOW CREATE SETTINGS PROFILE or SHOW CREATE PROFILE return p.parseShowCreateSettingsProfile(pos) @@ -4504,10 +4589,18 @@ func (p *Parser) parseShow() ast.Statement { } else if p.currentIs(token.USER) { show.ShowType = ast.ShowCreateUser p.nextToken() - // Skip user name and host pattern - they don't affect explain output - for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + // Skip user name and host pattern until FORMAT or end + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) && !p.currentIs(token.FORMAT) { p.nextToken() } + // Parse FORMAT clause if present + if p.currentIs(token.FORMAT) { + p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + show.Format = p.current.Value + p.nextToken() + } + } } else { show.ShowType = ast.ShowCreate // Handle SHOW CREATE TABLE, SHOW CREATE TEMPORARY TABLE, etc. @@ -4708,6 +4801,19 @@ func (p *Parser) parseSet() *ast.SetQuery { return set } +func (p *Parser) parseSetRole() *ast.SetRoleQuery { + query := &ast.SetRoleQuery{ + Position: p.current.Pos, + } + + // Skip SET DEFAULT ROLE ... TO ... + for !p.currentIs(token.EOF) && !p.currentIs(token.SEMICOLON) { + p.nextToken() + } + + return query +} + func (p *Parser) parseOptimize() *ast.OptimizeQuery { opt := &ast.OptimizeQuery{ Position: p.current.Pos, diff --git a/parser/testdata/00077_log_tinylog_stripelog/metadata.json b/parser/testdata/00077_log_tinylog_stripelog/metadata.json index 7234f858cd..0967ef424b 100644 --- a/parser/testdata/00077_log_tinylog_stripelog/metadata.json +++ b/parser/testdata/00077_log_tinylog_stripelog/metadata.json @@ -1,10 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/00716_allow_ddl/metadata.json b/parser/testdata/00716_allow_ddl/metadata.json index b7645733e4..dbdbb76d4f 100644 --- a/parser/testdata/00716_allow_ddl/metadata.json +++ b/parser/testdata/00716_allow_ddl/metadata.json @@ -1 +1,5 @@ -{"explain_todo":{"stmt5":true,"stmt6":true}} +{ + "explain_todo": { + "stmt6": true + } +} diff --git a/parser/testdata/01018_ddl_dictionaries_create/metadata.json b/parser/testdata/01018_ddl_dictionaries_create/metadata.json index f858c5980c..aa5bbb6eb4 100644 --- a/parser/testdata/01018_ddl_dictionaries_create/metadata.json +++ b/parser/testdata/01018_ddl_dictionaries_create/metadata.json @@ -1,15 +1,7 @@ { "explain_todo": { - "stmt13": true, "stmt17": true, - "stmt18": true, "stmt22": true, - "stmt23": true, - "stmt28": true, - "stmt37": true, - "stmt43": true, - "stmt48": true, - "stmt51": true, - "stmt53": true + "stmt43": true } } diff --git a/parser/testdata/01047_simple_aggregate_sizes_of_columns_bug/metadata.json b/parser/testdata/01047_simple_aggregate_sizes_of_columns_bug/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/01047_simple_aggregate_sizes_of_columns_bug/metadata.json +++ b/parser/testdata/01047_simple_aggregate_sizes_of_columns_bug/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} diff --git a/parser/testdata/01090_zookeeper_mutations_and_insert_quorum_long/metadata.json b/parser/testdata/01090_zookeeper_mutations_and_insert_quorum_long/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/01090_zookeeper_mutations_and_insert_quorum_long/metadata.json +++ b/parser/testdata/01090_zookeeper_mutations_and_insert_quorum_long/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} diff --git a/parser/testdata/01200_mutations_memory_consumption/metadata.json b/parser/testdata/01200_mutations_memory_consumption/metadata.json index 4a695f4ad6..0967ef424b 100644 --- a/parser/testdata/01200_mutations_memory_consumption/metadata.json +++ b/parser/testdata/01200_mutations_memory_consumption/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt19": true, - "stmt26": true, - "stmt5": true - } -} +{} diff --git a/parser/testdata/01292_create_user/metadata.json b/parser/testdata/01292_create_user/metadata.json index fd983695cd..0c27273b2d 100644 --- a/parser/testdata/01292_create_user/metadata.json +++ b/parser/testdata/01292_create_user/metadata.json @@ -1,12 +1,7 @@ { "explain_todo": { - "stmt157": true, - "stmt158": true, - "stmt159": true, - "stmt160": true, "stmt196": true, "stmt197": true, - "stmt200": true, "stmt201": true, "stmt221": true, "stmt239": true diff --git a/parser/testdata/01349_mutation_datetime_key/metadata.json b/parser/testdata/01349_mutation_datetime_key/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/01349_mutation_datetime_key/metadata.json +++ b/parser/testdata/01349_mutation_datetime_key/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/01358_mutation_delete_null_rows/metadata.json b/parser/testdata/01358_mutation_delete_null_rows/metadata.json index 7ad5569408..0967ef424b 100644 --- a/parser/testdata/01358_mutation_delete_null_rows/metadata.json +++ b/parser/testdata/01358_mutation_delete_null_rows/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt9": true - } -} +{} diff --git a/parser/testdata/01387_clear_column_default_depends/metadata.json b/parser/testdata/01387_clear_column_default_depends/metadata.json index 0411e1f140..13dfdf58b8 100644 --- a/parser/testdata/01387_clear_column_default_depends/metadata.json +++ b/parser/testdata/01387_clear_column_default_depends/metadata.json @@ -1,8 +1,5 @@ { "explain_todo": { - "stmt13": true, - "stmt21": true, - "stmt28": true, - "stmt5": true + "stmt28": true } } diff --git a/parser/testdata/01388_clear_all_columns/metadata.json b/parser/testdata/01388_clear_all_columns/metadata.json index d04cde097d..7ad5569408 100644 --- a/parser/testdata/01388_clear_all_columns/metadata.json +++ b/parser/testdata/01388_clear_all_columns/metadata.json @@ -1,11 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt13": true, - "stmt14": true, - "stmt4": true, - "stmt8": true, "stmt9": true } } diff --git a/parser/testdata/01493_alter_remove_no_property_zookeeper_long/metadata.json b/parser/testdata/01493_alter_remove_no_property_zookeeper_long/metadata.json index 522845c142..0967ef424b 100644 --- a/parser/testdata/01493_alter_remove_no_property_zookeeper_long/metadata.json +++ b/parser/testdata/01493_alter_remove_no_property_zookeeper_long/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt4": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/01493_alter_remove_properties/metadata.json b/parser/testdata/01493_alter_remove_properties/metadata.json index 633b943ce3..182f4d0e03 100644 --- a/parser/testdata/01493_alter_remove_properties/metadata.json +++ b/parser/testdata/01493_alter_remove_properties/metadata.json @@ -1,12 +1,6 @@ { "explain_todo": { - "stmt11": true, - "stmt16": true, "stmt2": true, - "stmt20": true, - "stmt24": true, - "stmt26": true, - "stmt7": true, - "stmt9": true + "stmt24": true } } diff --git a/parser/testdata/01493_alter_remove_properties_zookeeper/metadata.json b/parser/testdata/01493_alter_remove_properties_zookeeper/metadata.json index b748430a1f..c0a6c41fa3 100644 --- a/parser/testdata/01493_alter_remove_properties_zookeeper/metadata.json +++ b/parser/testdata/01493_alter_remove_properties_zookeeper/metadata.json @@ -1,9 +1,5 @@ { "explain_todo": { - "stmt11": true, - "stmt17": true, - "stmt21": true, - "stmt30": true, "stmt34": true, "stmt4": true, "stmt5": true diff --git a/parser/testdata/01493_alter_remove_wrong_default/metadata.json b/parser/testdata/01493_alter_remove_wrong_default/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/01493_alter_remove_wrong_default/metadata.json +++ b/parser/testdata/01493_alter_remove_wrong_default/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} diff --git a/parser/testdata/01497_mutation_support_for_storage_memory/metadata.json b/parser/testdata/01497_mutation_support_for_storage_memory/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/01497_mutation_support_for_storage_memory/metadata.json +++ b/parser/testdata/01497_mutation_support_for_storage_memory/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/01550_mutation_subquery/metadata.json b/parser/testdata/01550_mutation_subquery/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/01550_mutation_subquery/metadata.json +++ b/parser/testdata/01550_mutation_subquery/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/01603_remove_column_ttl/metadata.json b/parser/testdata/01603_remove_column_ttl/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/01603_remove_column_ttl/metadata.json +++ b/parser/testdata/01603_remove_column_ttl/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} diff --git a/parser/testdata/01630_simple_aggregate_all_functions_in_aggregating_merge_tree/metadata.json b/parser/testdata/01630_simple_aggregate_all_functions_in_aggregating_merge_tree/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/01630_simple_aggregate_all_functions_in_aggregating_merge_tree/metadata.json +++ b/parser/testdata/01630_simple_aggregate_all_functions_in_aggregating_merge_tree/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/01630_simple_aggregate_all_functions_in_summing_merge_tree/metadata.json b/parser/testdata/01630_simple_aggregate_all_functions_in_summing_merge_tree/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/01630_simple_aggregate_all_functions_in_summing_merge_tree/metadata.json +++ b/parser/testdata/01630_simple_aggregate_all_functions_in_summing_merge_tree/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/01702_system_query_log/metadata.json b/parser/testdata/01702_system_query_log/metadata.json index cb7a7d102e..0967ef424b 100644 --- a/parser/testdata/01702_system_query_log/metadata.json +++ b/parser/testdata/01702_system_query_log/metadata.json @@ -1,24 +1 @@ -{ - "explain_todo": { - "stmt19": true, - "stmt24": true, - "stmt28": true, - "stmt30": true, - "stmt34": true, - "stmt49": true, - "stmt50": true, - "stmt51": true, - "stmt52": true, - "stmt53": true, - "stmt54": true, - "stmt55": true, - "stmt56": true, - "stmt57": true, - "stmt58": true, - "stmt59": true, - "stmt60": true, - "stmt68": true, - "stmt69": true, - "stmt74": true - } -} +{} diff --git a/parser/testdata/01745_alter_delete_view/metadata.json b/parser/testdata/01745_alter_delete_view/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/01745_alter_delete_view/metadata.json +++ b/parser/testdata/01745_alter_delete_view/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} diff --git a/parser/testdata/01753_mutate_table_predicated_with_table/metadata.json b/parser/testdata/01753_mutate_table_predicated_with_table/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/01753_mutate_table_predicated_with_table/metadata.json +++ b/parser/testdata/01753_mutate_table_predicated_with_table/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} diff --git a/parser/testdata/01821_join_table_mutation/metadata.json b/parser/testdata/01821_join_table_mutation/metadata.json index 2cf5e97d8a..0967ef424b 100644 --- a/parser/testdata/01821_join_table_mutation/metadata.json +++ b/parser/testdata/01821_join_table_mutation/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt14": true, - "stmt16": true, - "stmt6": true - } -} +{} diff --git a/parser/testdata/01825_new_type_json_mutations/metadata.json b/parser/testdata/01825_new_type_json_mutations/metadata.json index f4c74e32be..0967ef424b 100644 --- a/parser/testdata/01825_new_type_json_mutations/metadata.json +++ b/parser/testdata/01825_new_type_json_mutations/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt10": true - } -} +{} diff --git a/parser/testdata/01851_clear_column_referenced_by_mv/metadata.json b/parser/testdata/01851_clear_column_referenced_by_mv/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/01851_clear_column_referenced_by_mv/metadata.json +++ b/parser/testdata/01851_clear_column_referenced_by_mv/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/01947_mv_subquery/metadata.json b/parser/testdata/01947_mv_subquery/metadata.json index 8eb3175658..0967ef424b 100644 --- a/parser/testdata/01947_mv_subquery/metadata.json +++ b/parser/testdata/01947_mv_subquery/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt17": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/02008_materialize_column/metadata.json b/parser/testdata/02008_materialize_column/metadata.json index c29be7f9f7..f2a7a3e6c2 100644 --- a/parser/testdata/02008_materialize_column/metadata.json +++ b/parser/testdata/02008_materialize_column/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt10": true, - "stmt13": true, "stmt14": true, "stmt22": true, "stmt25": true, diff --git a/parser/testdata/02026_describe_include_subcolumns/metadata.json b/parser/testdata/02026_describe_include_subcolumns/metadata.json index a4f8773303..bc5c6edb66 100644 --- a/parser/testdata/02026_describe_include_subcolumns/metadata.json +++ b/parser/testdata/02026_describe_include_subcolumns/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt3": true, - "stmt4": true, "stmt5": true } } diff --git a/parser/testdata/02286_vertical_merges_missed_column/metadata.json b/parser/testdata/02286_vertical_merges_missed_column/metadata.json index f4c74e32be..0967ef424b 100644 --- a/parser/testdata/02286_vertical_merges_missed_column/metadata.json +++ b/parser/testdata/02286_vertical_merges_missed_column/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt10": true - } -} +{} diff --git a/parser/testdata/02319_lightweight_delete_on_merge_tree/metadata.json b/parser/testdata/02319_lightweight_delete_on_merge_tree/metadata.json index 9103ba5f40..006ba4e250 100644 --- a/parser/testdata/02319_lightweight_delete_on_merge_tree/metadata.json +++ b/parser/testdata/02319_lightweight_delete_on_merge_tree/metadata.json @@ -6,7 +6,6 @@ "stmt30": true, "stmt45": true, "stmt52": true, - "stmt57": true, "stmt6": true, "stmt67": true, "stmt72": true, diff --git a/parser/testdata/02319_lightweight_delete_on_merge_tree_compact_parts/metadata.json b/parser/testdata/02319_lightweight_delete_on_merge_tree_compact_parts/metadata.json index 9849199b47..bd3fffc230 100644 --- a/parser/testdata/02319_lightweight_delete_on_merge_tree_compact_parts/metadata.json +++ b/parser/testdata/02319_lightweight_delete_on_merge_tree_compact_parts/metadata.json @@ -6,7 +6,6 @@ "stmt35": true, "stmt50": true, "stmt57": true, - "stmt62": true, "stmt7": true } } diff --git a/parser/testdata/02352_lightweight_delete/metadata.json b/parser/testdata/02352_lightweight_delete/metadata.json index 7cece2367e..9b85d6a428 100644 --- a/parser/testdata/02352_lightweight_delete/metadata.json +++ b/parser/testdata/02352_lightweight_delete/metadata.json @@ -4,7 +4,6 @@ "stmt19": true, "stmt29": true, "stmt34": true, - "stmt39": true, "stmt44": true, "stmt46": true, "stmt9": true diff --git a/parser/testdata/02416_rocksdb_delete_update/metadata.json b/parser/testdata/02416_rocksdb_delete_update/metadata.json index a63599a5bd..d2dcf8d7df 100644 --- a/parser/testdata/02416_rocksdb_delete_update/metadata.json +++ b/parser/testdata/02416_rocksdb_delete_update/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt12": true, "stmt21": true, - "stmt6": true, - "stmt9": true + "stmt6": true } } diff --git a/parser/testdata/02441_alter_delete_and_drop_column/metadata.json b/parser/testdata/02441_alter_delete_and_drop_column/metadata.json index 25122ac4f4..7ad5569408 100644 --- a/parser/testdata/02441_alter_delete_and_drop_column/metadata.json +++ b/parser/testdata/02441_alter_delete_and_drop_column/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt5": true, "stmt9": true } } diff --git a/parser/testdata/02494_query_cache_eligible_queries/metadata.json b/parser/testdata/02494_query_cache_eligible_queries/metadata.json index edf04afbb9..3529d26510 100644 --- a/parser/testdata/02494_query_cache_eligible_queries/metadata.json +++ b/parser/testdata/02494_query_cache_eligible_queries/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt17": true, "stmt23": true, "stmt25": true, "stmt27": true, diff --git a/parser/testdata/02521_lightweight_delete_and_ttl/metadata.json b/parser/testdata/02521_lightweight_delete_and_ttl/metadata.json index 47f980edf9..007cdea647 100644 --- a/parser/testdata/02521_lightweight_delete_and_ttl/metadata.json +++ b/parser/testdata/02521_lightweight_delete_and_ttl/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt15": true, "stmt18": true, "stmt5": true, "stmt9": true diff --git a/parser/testdata/02577_keepermap_delete_update/metadata.json b/parser/testdata/02577_keepermap_delete_update/metadata.json index a63599a5bd..d2dcf8d7df 100644 --- a/parser/testdata/02577_keepermap_delete_update/metadata.json +++ b/parser/testdata/02577_keepermap_delete_update/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt12": true, "stmt21": true, - "stmt6": true, - "stmt9": true + "stmt6": true } } diff --git a/parser/testdata/02581_share_big_sets_between_multiple_mutations_tasks_long/metadata.json b/parser/testdata/02581_share_big_sets_between_multiple_mutations_tasks_long/metadata.json index e91f8984c2..6a40e6e6e3 100644 --- a/parser/testdata/02581_share_big_sets_between_multiple_mutations_tasks_long/metadata.json +++ b/parser/testdata/02581_share_big_sets_between_multiple_mutations_tasks_long/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt12": true, "stmt16": true, "stmt2": true } diff --git a/parser/testdata/02675_sparse_columns_clear_column/metadata.json b/parser/testdata/02675_sparse_columns_clear_column/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/02675_sparse_columns_clear_column/metadata.json +++ b/parser/testdata/02675_sparse_columns_clear_column/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} diff --git a/parser/testdata/02707_keeper_map_delete_update_strict/metadata.json b/parser/testdata/02707_keeper_map_delete_update_strict/metadata.json index 8e865502d5..e4a9d201d5 100644 --- a/parser/testdata/02707_keeper_map_delete_update_strict/metadata.json +++ b/parser/testdata/02707_keeper_map_delete_update_strict/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt10": true, "stmt13": true, "stmt22": true, "stmt7": true diff --git a/parser/testdata/02734_sparse_columns_mutation/metadata.json b/parser/testdata/02734_sparse_columns_mutation/metadata.json index 25122ac4f4..3a06a4a1ac 100644 --- a/parser/testdata/02734_sparse_columns_mutation/metadata.json +++ b/parser/testdata/02734_sparse_columns_mutation/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt5": true, - "stmt9": true + "stmt5": true } } diff --git a/parser/testdata/02832_alter_delete_indexes_projections/metadata.json b/parser/testdata/02832_alter_delete_indexes_projections/metadata.json index f04990226e..0967ef424b 100644 --- a/parser/testdata/02832_alter_delete_indexes_projections/metadata.json +++ b/parser/testdata/02832_alter_delete_indexes_projections/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt14": true, - "stmt6": true - } -} +{} diff --git a/parser/testdata/02842_mutations_replace_non_deterministic/metadata.json b/parser/testdata/02842_mutations_replace_non_deterministic/metadata.json index 7389add715..fad6b6f917 100644 --- a/parser/testdata/02842_mutations_replace_non_deterministic/metadata.json +++ b/parser/testdata/02842_mutations_replace_non_deterministic/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt28": true, - "stmt41": true, "stmt42": true } } diff --git a/parser/testdata/02842_truncate_database/metadata.json b/parser/testdata/02842_truncate_database/metadata.json index 857cf4b2dc..7bf4b04abe 100644 --- a/parser/testdata/02842_truncate_database/metadata.json +++ b/parser/testdata/02842_truncate_database/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt32": true, - "stmt33": true, - "stmt43": true + "stmt33": true } } diff --git a/parser/testdata/02863_mutation_where_in_set_result_cache_pipeline_stuck_bug/metadata.json b/parser/testdata/02863_mutation_where_in_set_result_cache_pipeline_stuck_bug/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/02863_mutation_where_in_set_result_cache_pipeline_stuck_bug/metadata.json +++ b/parser/testdata/02863_mutation_where_in_set_result_cache_pipeline_stuck_bug/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/02870_per_column_settings/metadata.json b/parser/testdata/02870_per_column_settings/metadata.json index 4f6d7bf375..4db6ff0adf 100644 --- a/parser/testdata/02870_per_column_settings/metadata.json +++ b/parser/testdata/02870_per_column_settings/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt10": true, - "stmt13": true, "stmt16": true, "stmt2": true, "stmt20": true, diff --git a/parser/testdata/02889_print_pretty_type_names/metadata.json b/parser/testdata/02889_print_pretty_type_names/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/02889_print_pretty_type_names/metadata.json +++ b/parser/testdata/02889_print_pretty_type_names/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/02932_lwd_and_mutations/metadata.json b/parser/testdata/02932_lwd_and_mutations/metadata.json index 383575e459..57a221f1be 100644 --- a/parser/testdata/02932_lwd_and_mutations/metadata.json +++ b/parser/testdata/02932_lwd_and_mutations/metadata.json @@ -1,11 +1,7 @@ { "explain_todo": { "stmt11": true, - "stmt14": true, - "stmt17": true, - "stmt20": true, "stmt23": true, - "stmt5": true, - "stmt8": true + "stmt5": true } } diff --git a/parser/testdata/03047_on_fly_mutations_events/metadata.json b/parser/testdata/03047_on_fly_mutations_events/metadata.json index 2ef6a38008..ccc75f74d5 100644 --- a/parser/testdata/03047_on_fly_mutations_events/metadata.json +++ b/parser/testdata/03047_on_fly_mutations_events/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt15": true, - "stmt8": true, "stmt9": true } } diff --git a/parser/testdata/03047_on_fly_mutations_non_deterministic_replace/metadata.json b/parser/testdata/03047_on_fly_mutations_non_deterministic_replace/metadata.json index 0c54d3781a..6ba17be167 100644 --- a/parser/testdata/03047_on_fly_mutations_non_deterministic_replace/metadata.json +++ b/parser/testdata/03047_on_fly_mutations_non_deterministic_replace/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt36": true, - "stmt55": true, "stmt58": true } } diff --git a/parser/testdata/03047_on_fly_update_delete/metadata.json b/parser/testdata/03047_on_fly_update_delete/metadata.json index 56accf959b..1ed30c5543 100644 --- a/parser/testdata/03047_on_fly_update_delete/metadata.json +++ b/parser/testdata/03047_on_fly_update_delete/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt12": true, "stmt13": true, "stmt6": true } diff --git a/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json b/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json index f2b01e55b3..f216958458 100644 --- a/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json +++ b/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt11": true, "stmt14": true, - "stmt15": true, "stmt16": true, "stmt18": true, "stmt19": true, diff --git a/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json b/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json index b731c2f0d3..92efb02376 100644 --- a/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json +++ b/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { - "stmt10": true, "stmt6": true, - "stmt7": true, "stmt8": true } } diff --git a/parser/testdata/03100_lwu_deletes_2/metadata.json b/parser/testdata/03100_lwu_deletes_2/metadata.json index 1c7bcfb64f..a9bcd51630 100644 --- a/parser/testdata/03100_lwu_deletes_2/metadata.json +++ b/parser/testdata/03100_lwu_deletes_2/metadata.json @@ -3,7 +3,6 @@ "stmt10": true, "stmt18": true, "stmt30": true, - "stmt34": true, "stmt38": true } } diff --git a/parser/testdata/03148_mutations_virtual_columns/metadata.json b/parser/testdata/03148_mutations_virtual_columns/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/03148_mutations_virtual_columns/metadata.json +++ b/parser/testdata/03148_mutations_virtual_columns/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/03223_system_tables_set_not_ready/metadata.json b/parser/testdata/03223_system_tables_set_not_ready/metadata.json index 27369be079..9be7220609 100644 --- a/parser/testdata/03223_system_tables_set_not_ready/metadata.json +++ b/parser/testdata/03223_system_tables_set_not_ready/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt14": true, "stmt22": true } } diff --git a/parser/testdata/03250_avoid_prefetch_empty_parts/metadata.json b/parser/testdata/03250_avoid_prefetch_empty_parts/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/03250_avoid_prefetch_empty_parts/metadata.json +++ b/parser/testdata/03250_avoid_prefetch_empty_parts/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/03256_invalid_mutation_query/metadata.json b/parser/testdata/03256_invalid_mutation_query/metadata.json index b226c5e534..e18504f0c2 100644 --- a/parser/testdata/03256_invalid_mutation_query/metadata.json +++ b/parser/testdata/03256_invalid_mutation_query/metadata.json @@ -5,7 +5,6 @@ "stmt4": true, "stmt5": true, "stmt6": true, - "stmt7": true, "stmt9": true } } diff --git a/parser/testdata/03275_block_number_mutation/metadata.json b/parser/testdata/03275_block_number_mutation/metadata.json index 4a447480d5..0967ef424b 100644 --- a/parser/testdata/03275_block_number_mutation/metadata.json +++ b/parser/testdata/03275_block_number_mutation/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt19": true, - "stmt6": true - } -} +{} diff --git a/parser/testdata/03275_subcolumns_in_primary_key_bug/metadata.json b/parser/testdata/03275_subcolumns_in_primary_key_bug/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/03275_subcolumns_in_primary_key_bug/metadata.json +++ b/parser/testdata/03275_subcolumns_in_primary_key_bug/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/03325_alter_ast_format/metadata.json b/parser/testdata/03325_alter_ast_format/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/03325_alter_ast_format/metadata.json +++ b/parser/testdata/03325_alter_ast_format/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/03402_adding_projection_to_temporary_table/metadata.json b/parser/testdata/03402_adding_projection_to_temporary_table/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/03402_adding_projection_to_temporary_table/metadata.json +++ b/parser/testdata/03402_adding_projection_to_temporary_table/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} diff --git a/parser/testdata/03442_alter_delete_empty_part/metadata.json b/parser/testdata/03442_alter_delete_empty_part/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/03442_alter_delete_empty_part/metadata.json +++ b/parser/testdata/03442_alter_delete_empty_part/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} diff --git a/parser/testdata/03442_alter_delete_empty_part_rmt/metadata.json b/parser/testdata/03442_alter_delete_empty_part_rmt/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/03442_alter_delete_empty_part_rmt/metadata.json +++ b/parser/testdata/03442_alter_delete_empty_part_rmt/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/03442_lightweight_deletes_on_fly/metadata.json b/parser/testdata/03442_lightweight_deletes_on_fly/metadata.json index e3f4c45da5..d316b0c5fc 100644 --- a/parser/testdata/03442_lightweight_deletes_on_fly/metadata.json +++ b/parser/testdata/03442_lightweight_deletes_on_fly/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt13": true, "stmt16": true, - "stmt7": true, "stmt8": true } } diff --git a/parser/testdata/03636_storage_alias_basic/metadata.json b/parser/testdata/03636_storage_alias_basic/metadata.json index f778b3ba0e..fec152526a 100644 --- a/parser/testdata/03636_storage_alias_basic/metadata.json +++ b/parser/testdata/03636_storage_alias_basic/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt32": true, - "stmt55": true + "stmt32": true } } diff --git a/parser/testdata/03640_multiple_mutations_with_error_with_rewrite_parts/metadata.json b/parser/testdata/03640_multiple_mutations_with_error_with_rewrite_parts/metadata.json index 97c64968a0..6bf8d5b80a 100644 --- a/parser/testdata/03640_multiple_mutations_with_error_with_rewrite_parts/metadata.json +++ b/parser/testdata/03640_multiple_mutations_with_error_with_rewrite_parts/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt4": true, "stmt5": true, "stmt7": true } diff --git a/parser/testdata/03640_multiple_mutations_with_rewrite_parts/metadata.json b/parser/testdata/03640_multiple_mutations_with_rewrite_parts/metadata.json index 114ee38611..0967ef424b 100644 --- a/parser/testdata/03640_multiple_mutations_with_rewrite_parts/metadata.json +++ b/parser/testdata/03640_multiple_mutations_with_rewrite_parts/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/03653_updating_minmax_idx_after_mutation/metadata.json b/parser/testdata/03653_updating_minmax_idx_after_mutation/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/03653_updating_minmax_idx_after_mutation/metadata.json +++ b/parser/testdata/03653_updating_minmax_idx_after_mutation/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/03702_alter_column_update_and_delete_secondary_index_general/metadata.json b/parser/testdata/03702_alter_column_update_and_delete_secondary_index_general/metadata.json index badb15e09a..0967ef424b 100644 --- a/parser/testdata/03702_alter_column_update_and_delete_secondary_index_general/metadata.json +++ b/parser/testdata/03702_alter_column_update_and_delete_secondary_index_general/metadata.json @@ -1,12 +1 @@ -{ - "explain_todo": { - "stmt14": true, - "stmt16": true, - "stmt32": true, - "stmt34": true, - "stmt53": true, - "stmt55": true, - "stmt90": true, - "stmt92": true - } -} +{} diff --git a/parser/testdata/03704_function_dict_get_keys_cache_type/metadata.json b/parser/testdata/03704_function_dict_get_keys_cache_type/metadata.json index 023227a1d1..6b1cea3567 100644 --- a/parser/testdata/03704_function_dict_get_keys_cache_type/metadata.json +++ b/parser/testdata/03704_function_dict_get_keys_cache_type/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt10": true, - "stmt25": true, "stmt26": true } } diff --git a/parser/testdata/03727_tolowcardinality_nullable_cast/metadata.json b/parser/testdata/03727_tolowcardinality_nullable_cast/metadata.json index a954a15222..3a06a4a1ac 100644 --- a/parser/testdata/03727_tolowcardinality_nullable_cast/metadata.json +++ b/parser/testdata/03727_tolowcardinality_nullable_cast/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt10": true, "stmt5": true } } diff --git a/parser/testdata/03748_tuple_of_sparse_elements_bug/metadata.json b/parser/testdata/03748_tuple_of_sparse_elements_bug/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/03748_tuple_of_sparse_elements_bug/metadata.json +++ b/parser/testdata/03748_tuple_of_sparse_elements_bug/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} From 5bff0fef21fd4fd8ebf1cbd00f5a47e671dc180a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 16:15:16 +0000 Subject: [PATCH 12/22] Add special function transformations for DATE_ADD/SUB/DIFF and POSITION Implement ClickHouse-compatible transformations for special functions in the explain output: - DATE_ADD/DATEADD/TIMESTAMP_ADD/TIMESTAMPADD -> plus() - 3-arg form: (unit, n, date) -> plus(date, toIntervalUnit(n)) - 2-arg form: (interval, date) -> plus(interval, date) - DATE_SUB/DATESUB/TIMESTAMP_SUB/TIMESTAMPSUB -> minus() - 3-arg form: (unit, n, date) -> minus(date, toIntervalUnit(n)) - 2-arg form: (date, interval) -> minus(date, interval) - DATE_DIFF/DATEDIFF -> dateDiff() - (unit, date1, date2) -> dateDiff('unit', date1, date2) - POSITION with IN syntax: POSITION('ll' IN 'Hello') -> position('Hello', 'll') Also normalize POSITION function name to lowercase in all cases. Fixes 02160_special_functions and many other related tests. --- internal/explain/format.go | 1 + internal/explain/functions.go | 171 ++++++++++++++++++ .../metadata.json | 18 +- .../metadata.json | 3 - .../metadata.json | 5 - .../02160_special_functions/metadata.json | 24 +-- .../metadata.json | 6 - .../02724_persist_interval_type/metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 8 +- .../testdata/02814_age_datediff/metadata.json | 2 +- .../metadata.json | 2 - parser/testdata/03015_peder1001/metadata.json | 6 +- .../metadata.json | 6 +- 14 files changed, 180 insertions(+), 76 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index d0db04bdcd..402781573b 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -307,6 +307,7 @@ func NormalizeFunctionName(name string) string { "greatest": "greatest", "least": "least", "concat_ws": "concat", + "position": "position", // SQL standard ANY/ALL subquery operators "anymatch": "in", "allmatch": "notIn", diff --git a/internal/explain/functions.go b/internal/explain/functions.go index c4cbea74d5..0cc5b0859a 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -12,6 +12,11 @@ func explainFunctionCall(sb *strings.Builder, n *ast.FunctionCall, indent string } func explainFunctionCallWithAlias(sb *strings.Builder, n *ast.FunctionCall, alias string, indent string, depth int) { + // Handle special function transformations that ClickHouse does internally + if handled := handleSpecialFunction(sb, n, alias, indent, depth); handled { + return + } + children := 1 // arguments ExpressionList if len(n.Parameters) > 0 { children++ // parameters ExpressionList @@ -84,6 +89,172 @@ func windowSpecHasContent(w *ast.WindowSpec) bool { return false } +// handleSpecialFunction handles special function transformations that ClickHouse does internally. +// Returns true if the function was handled, false otherwise. +func handleSpecialFunction(sb *strings.Builder, n *ast.FunctionCall, alias string, indent string, depth int) bool { + fnName := strings.ToUpper(n.Name) + + // POSITION('ll' IN 'Hello') -> position('Hello', 'll') + if fnName == "POSITION" && len(n.Arguments) == 1 { + if inExpr, ok := n.Arguments[0].(*ast.InExpr); ok { + // Transform: POSITION(needle IN haystack) -> position(haystack, needle) + explainPositionWithIn(sb, inExpr.Expr, inExpr.List[0], alias, indent, depth) + return true + } + } + + // DATE_ADD/DATEADD/TIMESTAMP_ADD/TIMESTAMPADD + if fnName == "DATE_ADD" || fnName == "DATEADD" || fnName == "TIMESTAMP_ADD" || fnName == "TIMESTAMPADD" { + return handleDateAddSub(sb, n, alias, indent, depth, "plus") + } + + // DATE_SUB/DATESUB/TIMESTAMP_SUB/TIMESTAMPSUB + if fnName == "DATE_SUB" || fnName == "DATESUB" || fnName == "TIMESTAMP_SUB" || fnName == "TIMESTAMPSUB" { + return handleDateAddSub(sb, n, alias, indent, depth, "minus") + } + + // DATE_DIFF/DATEDIFF + if fnName == "DATE_DIFF" || fnName == "DATEDIFF" { + return handleDateDiff(sb, n, alias, indent, depth) + } + + return false +} + +// explainPositionWithIn outputs POSITION(needle IN haystack) as position(haystack, needle) +func explainPositionWithIn(sb *strings.Builder, needle, haystack ast.Expression, alias string, indent string, depth int) { + if alias != "" { + fmt.Fprintf(sb, "%sFunction position (alias %s) (children %d)\n", indent, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction position (children %d)\n", indent, 1) + } + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2) + // Arguments are swapped: haystack first, then needle + Node(sb, haystack, depth+2) + Node(sb, needle, depth+2) +} + +// handleDateAddSub handles DATE_ADD/DATE_SUB and variants +// opFunc is "plus" for ADD or "minus" for SUB +func handleDateAddSub(sb *strings.Builder, n *ast.FunctionCall, alias string, indent string, depth int, opFunc string) bool { + if len(n.Arguments) == 3 { + // DATE_ADD(unit, n, date) -> plus/minus(date, toIntervalUnit(n)) + unitArg := n.Arguments[0] + valueArg := n.Arguments[1] + dateArg := n.Arguments[2] + + // Extract unit from identifier + unitName := "" + if ident, ok := unitArg.(*ast.Identifier); ok { + unitName = ident.Name() + } + + if unitName != "" { + explainDateAddSubResult(sb, opFunc, dateArg, valueArg, unitName, alias, indent, depth) + return true + } + } else if len(n.Arguments) == 2 { + // DATE_ADD(interval, date) -> plus(interval, date) + // DATE_SUB(date, interval) -> minus(date, interval) + intervalArg := n.Arguments[0] + dateArg := n.Arguments[1] + + // Check which argument is the interval + if _, ok := intervalArg.(*ast.IntervalExpr); ok { + // Interval first: plus(interval, date) + explainDateAddSubWithInterval(sb, opFunc, intervalArg, dateArg, alias, indent, depth) + return true + } + // Check if first arg is already a toInterval function (from parser) + if fc, ok := intervalArg.(*ast.FunctionCall); ok && strings.HasPrefix(strings.ToLower(fc.Name), "tointerval") { + // Interval first: plus(interval, date) + explainDateAddSubWithInterval(sb, opFunc, intervalArg, dateArg, alias, indent, depth) + return true + } + + // DATE_SUB(date, interval) -> minus(date, interval) + if _, ok := dateArg.(*ast.IntervalExpr); ok { + explainDateAddSubWithInterval(sb, opFunc, intervalArg, dateArg, alias, indent, depth) + return true + } + if fc, ok := dateArg.(*ast.FunctionCall); ok && strings.HasPrefix(strings.ToLower(fc.Name), "tointerval") { + explainDateAddSubWithInterval(sb, opFunc, intervalArg, dateArg, alias, indent, depth) + return true + } + } + + return false +} + +// explainDateAddSubResult outputs the transformed DATE_ADD/SUB with unit syntax +func explainDateAddSubResult(sb *strings.Builder, opFunc string, dateArg, valueArg ast.Expression, unit string, alias string, indent string, depth int) { + if alias != "" { + fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, opFunc, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, opFunc, 1) + } + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2) + + // First arg: date + Node(sb, dateArg, depth+2) + + // Second arg: toIntervalUnit(value) + unitTitled := strings.ToUpper(unit[:1]) + strings.ToLower(unit[1:]) + fmt.Fprintf(sb, "%s Function toInterval%s (children %d)\n", indent, unitTitled, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) + Node(sb, valueArg, depth+4) +} + +// explainDateAddSubWithInterval outputs the transformed DATE_ADD/SUB with INTERVAL syntax +func explainDateAddSubWithInterval(sb *strings.Builder, opFunc string, arg1, arg2 ast.Expression, alias string, indent string, depth int) { + if alias != "" { + fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, opFunc, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, opFunc, 1) + } + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2) + Node(sb, arg1, depth+2) + Node(sb, arg2, depth+2) +} + +// handleDateDiff handles DATE_DIFF/DATEDIFF +// DATE_DIFF(unit, date1, date2) -> dateDiff('unit', date1, date2) +func handleDateDiff(sb *strings.Builder, n *ast.FunctionCall, alias string, indent string, depth int) bool { + if len(n.Arguments) != 3 { + return false + } + + unitArg := n.Arguments[0] + date1Arg := n.Arguments[1] + date2Arg := n.Arguments[2] + + // Extract unit from identifier + unitName := "" + if ident, ok := unitArg.(*ast.Identifier); ok { + unitName = ident.Name() + } + + if unitName == "" { + return false + } + + if alias != "" { + fmt.Fprintf(sb, "%sFunction dateDiff (alias %s) (children %d)\n", indent, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction dateDiff (children %d)\n", indent, 1) + } + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 3) + + // First arg: unit as lowercase string literal + fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, strings.ToLower(unitName)) + + // Second and third args: dates + Node(sb, date1Arg, depth+2) + Node(sb, date2Arg, depth+2) + + return true +} + func explainLambda(sb *strings.Builder, n *ast.Lambda, indent string, depth int) { explainLambdaWithAlias(sb, n, "", indent, depth) } diff --git a/parser/testdata/00233_position_function_sql_comparibilty/metadata.json b/parser/testdata/00233_position_function_sql_comparibilty/metadata.json index 503bf9ee0a..0967ef424b 100644 --- a/parser/testdata/00233_position_function_sql_comparibilty/metadata.json +++ b/parser/testdata/00233_position_function_sql_comparibilty/metadata.json @@ -1,17 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt2": true, - "stmt3": true, - "stmt4": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/00765_sql_compatibility_aliases/metadata.json b/parser/testdata/00765_sql_compatibility_aliases/metadata.json index 380408def5..d94ee08253 100644 --- a/parser/testdata/00765_sql_compatibility_aliases/metadata.json +++ b/parser/testdata/00765_sql_compatibility_aliases/metadata.json @@ -2,11 +2,8 @@ "explain_todo": { "stmt10": true, "stmt2": true, - "stmt25": true, "stmt26": true, - "stmt27": true, "stmt28": true, - "stmt29": true, "stmt3": true } } diff --git a/parser/testdata/01523_interval_operator_support_string_literal/metadata.json b/parser/testdata/01523_interval_operator_support_string_literal/metadata.json index e1ae16abb1..e572034190 100644 --- a/parser/testdata/01523_interval_operator_support_string_literal/metadata.json +++ b/parser/testdata/01523_interval_operator_support_string_literal/metadata.json @@ -4,12 +4,7 @@ "stmt15": true, "stmt18": true, "stmt19": true, - "stmt20": true, - "stmt21": true, - "stmt22": true, - "stmt23": true, "stmt24": true, - "stmt25": true, "stmt3": true, "stmt6": true, "stmt9": true diff --git a/parser/testdata/02160_special_functions/metadata.json b/parser/testdata/02160_special_functions/metadata.json index 6d66df8749..0967ef424b 100644 --- a/parser/testdata/02160_special_functions/metadata.json +++ b/parser/testdata/02160_special_functions/metadata.json @@ -1,23 +1 @@ -{ - "explain_todo": { - "stmt16": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt21": true, - "stmt22": true, - "stmt23": true, - "stmt24": true, - "stmt25": true, - "stmt26": true, - "stmt27": true, - "stmt28": true, - "stmt29": true, - "stmt30": true, - "stmt31": true, - "stmt32": true, - "stmt33": true, - "stmt34": true, - "stmt35": true - } -} +{} diff --git a/parser/testdata/02267_special_operator_parse_alias_check/metadata.json b/parser/testdata/02267_special_operator_parse_alias_check/metadata.json index b1214a8285..757ac1e443 100644 --- a/parser/testdata/02267_special_operator_parse_alias_check/metadata.json +++ b/parser/testdata/02267_special_operator_parse_alias_check/metadata.json @@ -2,17 +2,11 @@ "explain_todo": { "stmt41": true, "stmt43": true, - "stmt44": true, "stmt46": true, - "stmt47": true, "stmt48": true, - "stmt49": true, "stmt50": true, - "stmt51": true, "stmt52": true, - "stmt53": true, "stmt54": true, - "stmt55": true, "stmt56": true, "stmt57": true, "stmt58": true, diff --git a/parser/testdata/02724_persist_interval_type/metadata.json b/parser/testdata/02724_persist_interval_type/metadata.json index 48b547ca48..0967ef424b 100644 --- a/parser/testdata/02724_persist_interval_type/metadata.json +++ b/parser/testdata/02724_persist_interval_type/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt3":true,"stmt7":true}} +{} diff --git a/parser/testdata/02783_parsedatetimebesteffort_syslog/metadata.json b/parser/testdata/02783_parsedatetimebesteffort_syslog/metadata.json index aa21dd9600..0967ef424b 100644 --- a/parser/testdata/02783_parsedatetimebesteffort_syslog/metadata.json +++ b/parser/testdata/02783_parsedatetimebesteffort_syslog/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt11":true,"stmt7":true}} +{} diff --git a/parser/testdata/02806_cte_block_cannot_be_empty/metadata.json b/parser/testdata/02806_cte_block_cannot_be_empty/metadata.json index f7c9a031b3..0967ef424b 100644 --- a/parser/testdata/02806_cte_block_cannot_be_empty/metadata.json +++ b/parser/testdata/02806_cte_block_cannot_be_empty/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt2": true, - "stmt3": true - } -} +{} diff --git a/parser/testdata/02814_age_datediff/metadata.json b/parser/testdata/02814_age_datediff/metadata.json index 091a362101..0967ef424b 100644 --- a/parser/testdata/02814_age_datediff/metadata.json +++ b/parser/testdata/02814_age_datediff/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt55":true,"stmt56":true,"stmt57":true,"stmt58":true,"stmt59":true,"stmt60":true,"stmt61":true,"stmt62":true,"stmt63":true,"stmt64":true}} +{} diff --git a/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json b/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json index 7e650b7f29..790fda8db5 100644 --- a/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json +++ b/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json @@ -6,10 +6,8 @@ "stmt15": true, "stmt17": true, "stmt18": true, - "stmt19": true, "stmt2": true, "stmt20": true, - "stmt21": true, "stmt22": true, "stmt23": true, "stmt3": true, diff --git a/parser/testdata/03015_peder1001/metadata.json b/parser/testdata/03015_peder1001/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/03015_peder1001/metadata.json +++ b/parser/testdata/03015_peder1001/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/03176_check_timeout_in_index_analysis/metadata.json b/parser/testdata/03176_check_timeout_in_index_analysis/metadata.json index b563327205..0967ef424b 100644 --- a/parser/testdata/03176_check_timeout_in_index_analysis/metadata.json +++ b/parser/testdata/03176_check_timeout_in_index_analysis/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt7": true - } -} +{} From 3c0c79889a89fe8b71c72336a5da996a66a0f559 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 16:24:38 +0000 Subject: [PATCH 13/22] Add SHOW INDEX parsing and escape single quotes in identifiers - Add parsing for SHOW INDEX/INDEXES/INDICES/KEYS statements Maps to ShowColumns type as ClickHouse does internally - Handle SHOW EXTENDED INDEX syntax - Handle SHOW INDEX FROM table FROM database syntax - Add EscapeIdentifier function to escape single quotes as \' - Apply escaping to DropQuery and CreateQuery output Fixes 02724_show_indexes and related tests. --- internal/explain/format.go | 6 ++++ internal/explain/statements.go | 28 +++++++++---------- parser/parser.go | 27 ++++++++++++++++++ .../testdata/02706_show_columns/metadata.json | 6 ---- .../testdata/02724_show_indexes/metadata.json | 24 +--------------- 5 files changed, 48 insertions(+), 43 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index 402781573b..751418d804 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -37,6 +37,12 @@ func FormatFloat(val float64) string { return strconv.FormatFloat(val, 'f', -1, 64) } +// EscapeIdentifier escapes single quotes in identifiers for EXPLAIN AST output +// ClickHouse escapes ' as \' in identifier names +func EscapeIdentifier(s string) string { + return strings.ReplaceAll(s, "'", "\\'") +} + // escapeStringLiteral escapes special characters in a string for EXPLAIN AST output // Uses double-escaping as ClickHouse EXPLAIN AST displays strings // Iterates over bytes to preserve raw bytes (including invalid UTF-8) diff --git a/internal/explain/statements.go b/internal/explain/statements.go index 87182883f2..a84cc3a1fd 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -179,16 +179,16 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, } // ClickHouse adds an extra space before (children N) for CREATE DATABASE if n.CreateDatabase { - fmt.Fprintf(sb, "%sCreateQuery %s (children %d)\n", indent, name, children) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + fmt.Fprintf(sb, "%sCreateQuery %s (children %d)\n", indent, EscapeIdentifier(name), children) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) } else if hasDatabase { // Database-qualified: CreateQuery db table (children N) - fmt.Fprintf(sb, "%sCreateQuery %s %s (children %d)\n", indent, n.Database, name, children) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + fmt.Fprintf(sb, "%sCreateQuery %s %s (children %d)\n", indent, EscapeIdentifier(n.Database), EscapeIdentifier(name), children) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(n.Database)) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) } else { - fmt.Fprintf(sb, "%sCreateQuery %s (children %d)\n", indent, name, children) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + fmt.Fprintf(sb, "%sCreateQuery %s (children %d)\n", indent, EscapeIdentifier(name), children) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) } if len(n.Columns) > 0 || len(n.Indexes) > 0 || len(n.Projections) > 0 || len(n.Constraints) > 0 { childrenCount := 0 @@ -492,16 +492,16 @@ func explainDropQuery(sb *strings.Builder, n *ast.DropQuery, indent string, dept hasDatabase := n.Database != "" && !n.DropDatabase if hasDatabase { // Database-qualified: DropQuery db table (children 2) - fmt.Fprintf(sb, "%sDropQuery %s %s (children %d)\n", indent, n.Database, name, 2) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + fmt.Fprintf(sb, "%sDropQuery %s %s (children %d)\n", indent, EscapeIdentifier(n.Database), EscapeIdentifier(name), 2) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(n.Database)) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) } else if n.DropDatabase { // DROP DATABASE uses different spacing - fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, name, 1) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, EscapeIdentifier(name), 1) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) } else { - fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, name, 1) - fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, EscapeIdentifier(name), 1) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) } } diff --git a/parser/parser.go b/parser/parser.go index 7bbc4530a6..a2f5707e32 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -4549,6 +4549,10 @@ func (p *Parser) parseShow() ast.Statement { case token.COLUMNS: show.ShowType = ast.ShowColumns p.nextToken() + case token.INDEX: + // SHOW INDEX FROM table - treat as ShowColumns (ClickHouse maps to ShowColumns) + show.ShowType = ast.ShowColumns + p.nextToken() case token.CREATE: p.nextToken() if p.currentIs(token.DATABASE) { @@ -4625,11 +4629,25 @@ func (p *Parser) parseShow() ast.Statement { show.ShowType = ast.ShowDictionaries case "FUNCTIONS": show.ShowType = ast.ShowFunctions + case "INDEXES", "INDICES", "KEYS": + // SHOW INDEXES/INDICES/KEYS FROM table - treat as ShowColumns + show.ShowType = ast.ShowColumns + case "EXTENDED": + // SHOW EXTENDED INDEX FROM table - treat as ShowColumns + p.nextToken() + if p.currentIs(token.INDEX) { + p.nextToken() + } + show.ShowType = ast.ShowColumns + // Don't consume another token, fall through to FROM parsing + goto parseFrom } p.nextToken() } } +parseFrom: + // 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())) { @@ -4653,6 +4671,15 @@ func (p *Parser) parseShow() ast.Statement { } } + // Handle SHOW INDEX FROM table FROM database syntax (second FROM for database) + if p.currentIs(token.FROM) && show.ShowType == ast.ShowColumns { + p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + show.Database = p.current.Value + p.nextToken() + } + } + // Parse NOT LIKE, LIKE or ILIKE clause if p.currentIs(token.NOT) { p.nextToken() diff --git a/parser/testdata/02706_show_columns/metadata.json b/parser/testdata/02706_show_columns/metadata.json index 5c2629b23d..6fb75fafb6 100644 --- a/parser/testdata/02706_show_columns/metadata.json +++ b/parser/testdata/02706_show_columns/metadata.json @@ -1,13 +1,7 @@ { "explain_todo": { "stmt2": true, - "stmt35": true, - "stmt36": true, - "stmt37": true, - "stmt40": true, - "stmt41": true, "stmt5": true, - "stmt7": true, "stmt9": true } } diff --git a/parser/testdata/02724_show_indexes/metadata.json b/parser/testdata/02724_show_indexes/metadata.json index ed80134773..0967ef424b 100644 --- a/parser/testdata/02724_show_indexes/metadata.json +++ b/parser/testdata/02724_show_indexes/metadata.json @@ -1,23 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt15": true, - "stmt19": true, - "stmt23": true, - "stmt25": true, - "stmt26": true, - "stmt27": true, - "stmt28": true, - "stmt29": true, - "stmt30": true, - "stmt31": true, - "stmt37": true, - "stmt39": true, - "stmt4": true, - "stmt41": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt9": true - } -} +{} From efa0542fd35d4ea3fa722f1ff7f8249c3e9d42d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 16:40:36 +0000 Subject: [PATCH 14/22] Fix identifier and materialized view parsing for 03711 test - Fix lexer to handle identifiers starting with numbers after a dot (e.g., db.03711_table) - Add isIdentifierAfterDot() to distinguish 03711_table from decimal numbers - Fix TO clause in CREATE MATERIALIZED VIEW to support qualified names (database.table) - Add ToDatabase field to CreateQuery for materialized view targets --- ast/ast.go | 5 +- lexer/lexer.go | 55 +++++++++++++++++++ parser/parser.go | 9 ++- .../metadata.json | 24 +------- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 5cd10b2f48..b1cc60856f 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -254,8 +254,9 @@ type CreateQuery struct { Table string `json:"table,omitempty"` View string `json:"view,omitempty"` Materialized bool `json:"materialized,omitempty"` - To string `json:"to,omitempty"` // Target table for materialized views - Populate bool `json:"populate,omitempty"` // POPULATE for materialized views + ToDatabase string `json:"to_database,omitempty"` // Target database for materialized views + To string `json:"to,omitempty"` // Target table for materialized views + Populate bool `json:"populate,omitempty"` // POPULATE for materialized views Columns []*ColumnDeclaration `json:"columns,omitempty"` Indexes []*IndexDefinition `json:"indexes,omitempty"` Projections []*Projection `json:"projections,omitempty"` diff --git a/lexer/lexer.go b/lexer/lexer.go index 7335396657..1265e3849e 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -80,6 +80,55 @@ func (l *Lexer) skipWhitespace() { } } +// isIdentifierAfterDot checks if what follows the DOT looks like an identifier pattern. +// This handles cases like db.03711_table where 03711_table should be treated as an identifier. +// We look ahead to see if we have: digits followed by underscore followed by letter/digit +func (l *Lexer) isIdentifierAfterDot() bool { + // We're currently at '.', peek ahead to check the pattern + // Save current reader position by peeking + bytes, _ := l.reader.Peek(32) // peek enough to see the pattern, ignore EOF error + if len(bytes) == 0 { + return false + } + + idx := 0 + // Skip initial digits + for idx < len(bytes) && bytes[idx] >= '0' && bytes[idx] <= '9' { + idx++ + } + // If no digits found, it's not our pattern + if idx == 0 { + return false + } + // Check for underscore followed by letter/digit (identifier pattern) + if idx < len(bytes) && bytes[idx] == '_' { + idx++ + if idx < len(bytes) && isIdentContinueByte(bytes[idx]) { + return true + } + } + // Also check for letter directly after digits (like 1abc) + if idx < len(bytes) { + ch, _ := utf8.DecodeRune(bytes[idx:]) + // Check it's a letter but not exponent (e/E followed by digit/+/-) + if unicode.IsLetter(ch) { + if (ch == 'e' || ch == 'E') && idx+1 < len(bytes) { + next := bytes[idx+1] + if next >= '0' && next <= '9' || next == '+' || next == '-' { + return false // This is a scientific notation number + } + } + return true + } + } + return false +} + +// isIdentContinueByte checks if a byte is a valid identifier continuation character +func isIdentContinueByte(b byte) bool { + return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_' +} + // isClickHouseWhitespace returns true for characters ClickHouse treats as whitespace // but Go's unicode.IsSpace does not recognize. func isClickHouseWhitespace(ch rune) bool { @@ -226,6 +275,12 @@ func (l *Lexer) NextToken() Item { return Item{Token: token.COMMA, Value: ",", Pos: pos} case '.': if unicode.IsDigit(l.peekChar()) { + // Before treating .digits as a number, check if it's part of an identifier pattern + // For example, db.03711_table should be DOT + IDENT, not DOT + NUMBER + IDENT + if l.isIdentifierAfterDot() { + l.readChar() + return Item{Token: token.DOT, Value: ".", Pos: pos} + } return l.readNumber() } l.readChar() diff --git a/parser/parser.go b/parser/parser.go index a2f5707e32..b410331299 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1975,7 +1975,14 @@ func (p *Parser) parseCreateView(create *ast.CreateQuery) { return } p.nextToken() - create.To = p.parseIdentifierName() + toName := p.parseIdentifierName() + if p.currentIs(token.DOT) { + p.nextToken() + create.ToDatabase = toName + create.To = p.parseIdentifierName() + } else { + create.To = toName + } } // Parse ENGINE (for materialized views) diff --git a/parser/testdata/03711_deduplication_blocks_part_log/metadata.json b/parser/testdata/03711_deduplication_blocks_part_log/metadata.json index 7bde622007..0967ef424b 100644 --- a/parser/testdata/03711_deduplication_blocks_part_log/metadata.json +++ b/parser/testdata/03711_deduplication_blocks_part_log/metadata.json @@ -1,23 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt27": true, - "stmt3": true, - "stmt4": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} From 9239c041f9d2b2a447374b81edfc710b599c2484 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:01:13 +0000 Subject: [PATCH 15/22] Fix :: cast formatting for arrays in EXPLAIN AST - Arrays with numeric primitives use string format ('[1, 2, 3]') in :: casts - Arrays with booleans use Array_[Bool_0, Bool_1] format - Arrays with NULLs use Array_[NULL, ...] format - Arrays with strings use string format ('[\'foo\', \'bar\']') - Aliases are always shown for :: cast syntax with arrays/tuples This fixes 15+ statements in 02708_dotProduct and many other tests. --- internal/explain/functions.go | 99 +++++++++++++++---- parser/testdata/00727_concat/metadata.json | 16 ++- .../testdata/00926_multimatch/metadata.json | 11 +-- .../01602_array_aggregation/metadata.json | 9 +- .../01852_cast_operator/metadata.json | 7 +- .../01852_cast_operator_2/metadata.json | 5 - .../metadata.json | 7 +- .../02405_avro_read_nested/metadata.json | 7 +- .../metadata.json | 7 +- .../02495_concat_with_separator/metadata.json | 7 +- .../metadata.json | 15 +-- .../metadata.json | 6 +- .../02685_decimal256_various/metadata.json | 8 +- .../testdata/02708_dotProduct/metadata.json | 17 +--- .../metadata.json | 1 - .../metadata.json | 16 ++- .../02981_nested_bad_types/metadata.json | 2 +- .../02990_arrayFold_nullable_lc/metadata.json | 12 +-- .../02996_nullable_arrayReduce/metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 1 - .../metadata.json | 12 +-- .../metadata.json | 18 +--- .../metadata.json | 1 - .../metadata.json | 6 +- .../metadata.json | 7 -- .../03532_json_dynamic_updates/metadata.json | 7 +- parser/testdata/03533_xirr/metadata.json | 6 +- .../testdata/03538_array_except/metadata.json | 2 - .../metadata.json | 9 +- .../metadata.json | 13 +-- 31 files changed, 139 insertions(+), 199 deletions(-) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 0cc5b0859a..b03c3792fb 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -287,19 +287,18 @@ func explainCastExpr(sb *strings.Builder, n *ast.CastExpr, indent string, depth } func explainCastExprWithAlias(sb *strings.Builder, n *ast.CastExpr, alias string, indent string, depth int) { - // For :: operator syntax, ClickHouse hides alias only when expression is - // an array/tuple with complex content that gets formatted as string - hideAlias := false + // For :: operator syntax with arrays/tuples, determine formatting based on content useArrayFormat := false if n.OperatorSyntax { if lit, ok := n.Expr.(*ast.Literal); ok { if lit.Type == ast.LiteralArray || lit.Type == ast.LiteralTuple { // Determine format based on both content and target type useArrayFormat = shouldUseArrayFormat(lit, n.Type) - hideAlias = !useArrayFormat } } } + // Alias is always shown for :: cast syntax with arrays/tuples + hideAlias := false // CAST is represented as Function CAST with expr and type as arguments if alias != "" && !hideAlias { @@ -354,31 +353,97 @@ func explainCastExprWithAlias(sb *strings.Builder, n *ast.CastExpr, alias string // shouldUseArrayFormat determines whether to use Array_[...] format or string format // for array/tuple literals in :: cast expressions. -// This depends on both the literal content and the target type. +// ClickHouse uses different formats depending on element types: +// - Boolean arrays: Array_[Bool_0, Bool_1] format +// - Numeric arrays: '[1, 2, 3]' string format func shouldUseArrayFormat(lit *ast.Literal, targetType *ast.DataType) bool { // First check if the literal contains only primitive literals (not expressions) if !containsOnlyLiterals(lit) { return false } - // For arrays of strings, check the target type to determine format + // Check if array contains boolean elements - these use Array_ format + if containsBooleanElements(lit) { + return true + } + + // Check if array contains NULL elements - these use Array_ format + if containsNullElements(lit) { + return true + } + + // For arrays of strings, always use string format in :: casts + // This applies to all target types including Array(String) if lit.Type == ast.LiteralArray && hasStringElements(lit) { - // Only use Array_ format when casting to Array(String) specifically - // For other types like Array(JSON), Array(LowCardinality(String)), etc., use string format - if targetType != nil && strings.ToLower(targetType.Name) == "array" && len(targetType.Parameters) > 0 { - if innerType, ok := targetType.Parameters[0].(*ast.DataType); ok { - // Only use Array_ format if inner type is exactly "String" with no parameters - if strings.ToLower(innerType.Name) == "string" && len(innerType.Parameters) == 0 { - return true - } + return false + } + + // For numeric primitives, use string format in :: casts + return false +} + +// containsNullElements checks if a literal array/tuple contains NULL elements +func containsNullElements(lit *ast.Literal) bool { + var exprs []ast.Expression + switch lit.Type { + case ast.LiteralArray, ast.LiteralTuple: + var ok bool + exprs, ok = lit.Value.([]ast.Expression) + if !ok { + return false + } + default: + return false + } + + for _, e := range exprs { + innerLit, ok := e.(*ast.Literal) + if !ok { + continue + } + if innerLit.Type == ast.LiteralNull { + return true + } + // Check nested arrays/tuples + if innerLit.Type == ast.LiteralArray || innerLit.Type == ast.LiteralTuple { + if containsNullElements(innerLit) { + return true } } - // For any other type (JSON, LowCardinality, etc.), use string format + } + return false +} + +// containsBooleanElements checks if a literal array/tuple contains boolean elements +func containsBooleanElements(lit *ast.Literal) bool { + var exprs []ast.Expression + switch lit.Type { + case ast.LiteralArray, ast.LiteralTuple: + var ok bool + exprs, ok = lit.Value.([]ast.Expression) + if !ok { + return false + } + default: return false } - // For non-string primitives, always use Array_ format - return true + for _, e := range exprs { + innerLit, ok := e.(*ast.Literal) + if !ok { + continue + } + if innerLit.Type == ast.LiteralBoolean { + return true + } + // Check nested arrays/tuples + if innerLit.Type == ast.LiteralArray || innerLit.Type == ast.LiteralTuple { + if containsBooleanElements(innerLit) { + return true + } + } + } + return false } // containsOnlyLiterals checks if a literal array/tuple contains only literal values (no expressions) diff --git a/parser/testdata/00727_concat/metadata.json b/parser/testdata/00727_concat/metadata.json index 7082dff93a..36ee3e64c6 100644 --- a/parser/testdata/00727_concat/metadata.json +++ b/parser/testdata/00727_concat/metadata.json @@ -1 +1,15 @@ -{"explain_todo":{"stmt19":true,"stmt20":true,"stmt37":true,"stmt39":true,"stmt43":true,"stmt44":true,"stmt45":true,"stmt46":true,"stmt62":true,"stmt63":true,"stmt64":true,"stmt65":true,"stmt66":true,"stmt67":true,"stmt83":true}} +{ + "explain_todo": { + "stmt19": true, + "stmt20": true, + "stmt44": true, + "stmt46": true, + "stmt62": true, + "stmt63": true, + "stmt64": true, + "stmt65": true, + "stmt66": true, + "stmt67": true, + "stmt83": true + } +} diff --git a/parser/testdata/00926_multimatch/metadata.json b/parser/testdata/00926_multimatch/metadata.json index 01cf331164..0967ef424b 100644 --- a/parser/testdata/00926_multimatch/metadata.json +++ b/parser/testdata/00926_multimatch/metadata.json @@ -1,10 +1 @@ -{ - "explain_todo": { - "stmt111": true, - "stmt151": true, - "stmt158": true, - "stmt28": true, - "stmt68": true, - "stmt75": true - } -} +{} diff --git a/parser/testdata/01602_array_aggregation/metadata.json b/parser/testdata/01602_array_aggregation/metadata.json index 8dae11f1e5..0967ef424b 100644 --- a/parser/testdata/01602_array_aggregation/metadata.json +++ b/parser/testdata/01602_array_aggregation/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt32": true, - "stmt33": true, - "stmt34": true, - "stmt35": true - } -} +{} diff --git a/parser/testdata/01852_cast_operator/metadata.json b/parser/testdata/01852_cast_operator/metadata.json index 718b43bd01..a38745fbb0 100644 --- a/parser/testdata/01852_cast_operator/metadata.json +++ b/parser/testdata/01852_cast_operator/metadata.json @@ -2,11 +2,6 @@ "explain_todo": { "stmt11": true, "stmt12": true, - "stmt17": true, - "stmt29": true, - "stmt3": true, - "stmt30": true, - "stmt31": true, - "stmt4": true + "stmt29": true } } diff --git a/parser/testdata/01852_cast_operator_2/metadata.json b/parser/testdata/01852_cast_operator_2/metadata.json index eaeb241fd9..c0cad435f1 100644 --- a/parser/testdata/01852_cast_operator_2/metadata.json +++ b/parser/testdata/01852_cast_operator_2/metadata.json @@ -1,13 +1,8 @@ { "explain_todo": { - "stmt1": true, "stmt10": true, "stmt11": true, "stmt12": true, - "stmt13": true, - "stmt2": true, - "stmt5": true, - "stmt6": true, "stmt7": true, "stmt8": true, "stmt9": true diff --git a/parser/testdata/02116_tuple_element_analyzer/metadata.json b/parser/testdata/02116_tuple_element_analyzer/metadata.json index 5452d4a529..0967ef424b 100644 --- a/parser/testdata/02116_tuple_element_analyzer/metadata.json +++ b/parser/testdata/02116_tuple_element_analyzer/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt30": true, - "stmt31": true - } -} +{} diff --git a/parser/testdata/02405_avro_read_nested/metadata.json b/parser/testdata/02405_avro_read_nested/metadata.json index 9a8cc69c0b..0967ef424b 100644 --- a/parser/testdata/02405_avro_read_nested/metadata.json +++ b/parser/testdata/02405_avro_read_nested/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt2": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/02423_multidimensional_array_get_data_at/metadata.json b/parser/testdata/02423_multidimensional_array_get_data_at/metadata.json index 8b0256d0ad..0967ef424b 100644 --- a/parser/testdata/02423_multidimensional_array_get_data_at/metadata.json +++ b/parser/testdata/02423_multidimensional_array_get_data_at/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt4": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/02495_concat_with_separator/metadata.json b/parser/testdata/02495_concat_with_separator/metadata.json index a313391ed3..d689cec729 100644 --- a/parser/testdata/02495_concat_with_separator/metadata.json +++ b/parser/testdata/02495_concat_with_separator/metadata.json @@ -1 +1,6 @@ -{"explain_todo":{"stmt40":true,"stmt41":true,"stmt57":true,"stmt58":true}} +{ + "explain_todo": { + "stmt40": true, + "stmt41": true + } +} diff --git a/parser/testdata/02499_quantile_nan_ubsan_msan/metadata.json b/parser/testdata/02499_quantile_nan_ubsan_msan/metadata.json index c112bf01c6..0967ef424b 100644 --- a/parser/testdata/02499_quantile_nan_ubsan_msan/metadata.json +++ b/parser/testdata/02499_quantile_nan_ubsan_msan/metadata.json @@ -1,14 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt14": true, - "stmt15": true, - "stmt17": true, - "stmt19": true, - "stmt3": true, - "stmt5": true, - "stmt6": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/02677_analyzer_compound_expressions/metadata.json b/parser/testdata/02677_analyzer_compound_expressions/metadata.json index b0ad0d1a0e..f4c74e32be 100644 --- a/parser/testdata/02677_analyzer_compound_expressions/metadata.json +++ b/parser/testdata/02677_analyzer_compound_expressions/metadata.json @@ -1,9 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt2": true, - "stmt3": true, - "stmt4": true, - "stmt5": true + "stmt10": true } } diff --git a/parser/testdata/02685_decimal256_various/metadata.json b/parser/testdata/02685_decimal256_various/metadata.json index 6e2f58a192..abe45ba24a 100644 --- a/parser/testdata/02685_decimal256_various/metadata.json +++ b/parser/testdata/02685_decimal256_various/metadata.json @@ -1,11 +1,5 @@ { "explain_todo": { - "stmt27": true, - "stmt30": true, - "stmt31": true, - "stmt32": true, - "stmt33": true, - "stmt34": true, - "stmt35": true + "stmt27": true } } diff --git a/parser/testdata/02708_dotProduct/metadata.json b/parser/testdata/02708_dotProduct/metadata.json index 435ccb1955..c594a0f078 100644 --- a/parser/testdata/02708_dotProduct/metadata.json +++ b/parser/testdata/02708_dotProduct/metadata.json @@ -1,23 +1,8 @@ { "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt21": true, "stmt34": true, - "stmt35": true, - "stmt36": true, "stmt38": true, "stmt54": true, - "stmt55": true, - "stmt56": true + "stmt55": true } } diff --git a/parser/testdata/02813_optimize_lazy_materialization/metadata.json b/parser/testdata/02813_optimize_lazy_materialization/metadata.json index 5a6e7bbecd..c7108ca34e 100644 --- a/parser/testdata/02813_optimize_lazy_materialization/metadata.json +++ b/parser/testdata/02813_optimize_lazy_materialization/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt118": true, "stmt136": true, "stmt141": true } diff --git a/parser/testdata/02935_format_with_arbitrary_types/metadata.json b/parser/testdata/02935_format_with_arbitrary_types/metadata.json index 4fc28f6ddf..4024074f48 100644 --- a/parser/testdata/02935_format_with_arbitrary_types/metadata.json +++ b/parser/testdata/02935_format_with_arbitrary_types/metadata.json @@ -1 +1,15 @@ -{"explain_todo":{"stmt19":true,"stmt20":true,"stmt37":true,"stmt39":true,"stmt43":true,"stmt44":true,"stmt45":true,"stmt46":true,"stmt55":true,"stmt56":true,"stmt57":true,"stmt58":true,"stmt59":true,"stmt60":true,"stmt76":true}} +{ + "explain_todo": { + "stmt19": true, + "stmt20": true, + "stmt44": true, + "stmt46": true, + "stmt55": true, + "stmt56": true, + "stmt57": true, + "stmt58": true, + "stmt59": true, + "stmt60": true, + "stmt76": true + } +} diff --git a/parser/testdata/02981_nested_bad_types/metadata.json b/parser/testdata/02981_nested_bad_types/metadata.json index 8f2a6eb399..0967ef424b 100644 --- a/parser/testdata/02981_nested_bad_types/metadata.json +++ b/parser/testdata/02981_nested_bad_types/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt17":true,"stmt3":true,"stmt4":true}} +{} diff --git a/parser/testdata/02990_arrayFold_nullable_lc/metadata.json b/parser/testdata/02990_arrayFold_nullable_lc/metadata.json index 76d43ac97c..af67e43fa8 100644 --- a/parser/testdata/02990_arrayFold_nullable_lc/metadata.json +++ b/parser/testdata/02990_arrayFold_nullable_lc/metadata.json @@ -1,16 +1,6 @@ { "explain_todo": { - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt21": true, - "stmt22": true, "stmt24": true, - "stmt25": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true + "stmt25": true } } diff --git a/parser/testdata/02996_nullable_arrayReduce/metadata.json b/parser/testdata/02996_nullable_arrayReduce/metadata.json index a62af59386..0967ef424b 100644 --- a/parser/testdata/02996_nullable_arrayReduce/metadata.json +++ b/parser/testdata/02996_nullable_arrayReduce/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt6":true,"stmt7":true}} +{} diff --git a/parser/testdata/02999_variant_suspicious_types/metadata.json b/parser/testdata/02999_variant_suspicious_types/metadata.json index cc22e4816f..0967ef424b 100644 --- a/parser/testdata/02999_variant_suspicious_types/metadata.json +++ b/parser/testdata/02999_variant_suspicious_types/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt3":true,"stmt5":true}} +{} diff --git a/parser/testdata/03011_definitive_guide_to_cast/metadata.json b/parser/testdata/03011_definitive_guide_to_cast/metadata.json index 48544c5324..90500807fc 100644 --- a/parser/testdata/03011_definitive_guide_to_cast/metadata.json +++ b/parser/testdata/03011_definitive_guide_to_cast/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt36": true, - "stmt52": true, "stmt53": true, "stmt84": true, "stmt86": true diff --git a/parser/testdata/03208_groupArrayIntersect_serialization/metadata.json b/parser/testdata/03208_groupArrayIntersect_serialization/metadata.json index 6bcb344c6d..0967ef424b 100644 --- a/parser/testdata/03208_groupArrayIntersect_serialization/metadata.json +++ b/parser/testdata/03208_groupArrayIntersect_serialization/metadata.json @@ -1,11 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt13": true, - "stmt15": true, - "stmt28": true, - "stmt5": true, - "stmt7": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03254_timeseries_functions_various_arguments/metadata.json b/parser/testdata/03254_timeseries_functions_various_arguments/metadata.json index 904bce3282..0967ef424b 100644 --- a/parser/testdata/03254_timeseries_functions_various_arguments/metadata.json +++ b/parser/testdata/03254_timeseries_functions_various_arguments/metadata.json @@ -1,17 +1 @@ -{ - "explain_todo": { - "stmt44": true, - "stmt46": true, - "stmt47": true, - "stmt48": true, - "stmt49": true, - "stmt50": true, - "stmt51": true, - "stmt52": true, - "stmt53": true, - "stmt54": true, - "stmt55": true, - "stmt56": true, - "stmt61": true - } -} +{} diff --git a/parser/testdata/03254_timeseries_to_grid_aggregate_function/metadata.json b/parser/testdata/03254_timeseries_to_grid_aggregate_function/metadata.json index dd69e2d46b..523415a672 100644 --- a/parser/testdata/03254_timeseries_to_grid_aggregate_function/metadata.json +++ b/parser/testdata/03254_timeseries_to_grid_aggregate_function/metadata.json @@ -4,7 +4,6 @@ "stmt11": true, "stmt12": true, "stmt2": true, - "stmt23": true, "stmt3": true, "stmt8": true, "stmt9": true diff --git a/parser/testdata/03357_arraySymmetricDifference/metadata.json b/parser/testdata/03357_arraySymmetricDifference/metadata.json index a08759fb21..0967ef424b 100644 --- a/parser/testdata/03357_arraySymmetricDifference/metadata.json +++ b/parser/testdata/03357_arraySymmetricDifference/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt21": true - } -} +{} diff --git a/parser/testdata/03522_function_first_non_default/metadata.json b/parser/testdata/03522_function_first_non_default/metadata.json index 8e042738b6..568bfbc472 100644 --- a/parser/testdata/03522_function_first_non_default/metadata.json +++ b/parser/testdata/03522_function_first_non_default/metadata.json @@ -2,17 +2,10 @@ "explain_todo": { "stmt16": true, "stmt2": true, - "stmt20": true, - "stmt21": true, "stmt22": true, "stmt23": true, - "stmt27": true, - "stmt30": true, - "stmt31": true, "stmt33": true, "stmt4": true, - "stmt40": true, - "stmt6": true, "stmt7": true } } diff --git a/parser/testdata/03532_json_dynamic_updates/metadata.json b/parser/testdata/03532_json_dynamic_updates/metadata.json index c0f43c5c6d..0967ef424b 100644 --- a/parser/testdata/03532_json_dynamic_updates/metadata.json +++ b/parser/testdata/03532_json_dynamic_updates/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt33": true, - "stmt6": true - } -} +{} diff --git a/parser/testdata/03533_xirr/metadata.json b/parser/testdata/03533_xirr/metadata.json index d4d1d99f95..0967ef424b 100644 --- a/parser/testdata/03533_xirr/metadata.json +++ b/parser/testdata/03533_xirr/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt14": true - } -} +{} diff --git a/parser/testdata/03538_array_except/metadata.json b/parser/testdata/03538_array_except/metadata.json index ad55ed3111..dc0702ac62 100644 --- a/parser/testdata/03538_array_except/metadata.json +++ b/parser/testdata/03538_array_except/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt11": true, "stmt30": true } } diff --git a/parser/testdata/03573_linear_regression_timeseries_functions_various_arguments/metadata.json b/parser/testdata/03573_linear_regression_timeseries_functions_various_arguments/metadata.json index e74ad9f4ca..0967ef424b 100644 --- a/parser/testdata/03573_linear_regression_timeseries_functions_various_arguments/metadata.json +++ b/parser/testdata/03573_linear_regression_timeseries_functions_various_arguments/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt30": true, - "stmt32": true, - "stmt33": true, - "stmt34": true - } -} +{} diff --git a/parser/testdata/03595_changes_timeseries_functions_various_arguments/metadata.json b/parser/testdata/03595_changes_timeseries_functions_various_arguments/metadata.json index 5b9a4a1d70..0967ef424b 100644 --- a/parser/testdata/03595_changes_timeseries_functions_various_arguments/metadata.json +++ b/parser/testdata/03595_changes_timeseries_functions_various_arguments/metadata.json @@ -1,12 +1 @@ -{ - "explain_todo": { - "stmt22": true, - "stmt24": true, - "stmt26": true, - "stmt27": true, - "stmt28": true, - "stmt29": true, - "stmt31": true, - "stmt32": true - } -} +{} From b622b657b3fe2a9adbe0dba28dedabb1ac614d9c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:20:39 +0000 Subject: [PATCH 16/22] Add Parenthesized flag to Lambda AST node to fix multi-param lambda merging When parsing multi-param lambdas like `acc,x -> body` where parameters are separated by commas without parentheses, the parser was incorrectly merging preceding identifiers with explicitly parenthesized lambdas like `(x -> y)`. This fix: - Adds `Parenthesized bool` field to ast.Lambda - Sets this flag when a lambda is wrapped in explicit parentheses - Checks the flag in mergeMultiParamLambdas to skip parenthesized lambdas This correctly parses: - `arrayFold(acc,x -> body, arr, init)` - merges acc into lambda params - `delay(time, (time -> 0.5), ...)` - keeps time as separate argument Fixes all 17 statements in 02718_array_fold test and 3 in 02418_aggregate_combinators. --- ast/ast.go | 7 ++- parser/expression.go | 59 +++++++++++++++++++ .../02418_aggregate_combinators/metadata.json | 8 +-- .../testdata/02718_array_fold/metadata.json | 22 +------ 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index b1cc60856f..d8e77b81bb 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -1440,9 +1440,10 @@ func (t *TupleAccess) expressionNode() {} // Lambda represents a lambda expression. type Lambda struct { - Position token.Position `json:"-"` - Parameters []string `json:"parameters"` - Body Expression `json:"body"` + Position token.Position `json:"-"` + Parameters []string `json:"parameters"` + Body Expression `json:"body"` + Parenthesized bool `json:"-"` // True if wrapped in explicit parentheses } func (l *Lambda) Pos() token.Position { return l.Position } diff --git a/parser/expression.go b/parser/expression.go index 9ef6eb6112..9e07a65e68 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -158,6 +158,58 @@ func (p *Parser) parseFunctionArgumentList() []ast.Expression { } } + // Post-process: merge consecutive identifiers followed by a lambda into a multi-param lambda + // Pattern: [Ident("acc"), Lambda(["x"], body)] -> [Lambda(["acc", "x"], body)] + exprs = mergeMultiParamLambdas(exprs) + + return exprs +} + +// mergeMultiParamLambdas looks for pattern [Ident, Ident, ..., Lambda] at the START +// of the expression list and merges them into a single multi-param lambda. +// This handles ClickHouse's syntax: acc,x -> body (multi-param lambda without parentheses) +// This ONLY applies at position 0 - identifiers in the middle are regular arguments. +func mergeMultiParamLambdas(exprs []ast.Expression) []ast.Expression { + if len(exprs) < 2 { + return exprs + } + + // Only check at position 0 - the pattern must start at the beginning + if ident, ok := exprs[0].(*ast.Identifier); ok && len(ident.Parts) == 1 { + // Count consecutive simple identifiers at the start + j := 0 + var params []string + for j < len(exprs) { + if id, ok := exprs[j].(*ast.Identifier); ok && len(id.Parts) == 1 { + params = append(params, id.Name()) + j++ + } else { + break + } + } + // Check if the next expression is a lambda and we have at least one identifier + if j < len(exprs) && len(params) >= 1 { + if lambda, ok := exprs[j].(*ast.Lambda); ok { + // Don't merge if lambda was explicitly parenthesized + // e.g., f(a, (x -> y)) should NOT merge 'a' into the lambda + if lambda.Parenthesized { + return exprs + } + // Merge the identifiers into the lambda's parameters + newParams := make([]string, 0, len(params)+len(lambda.Parameters)) + newParams = append(newParams, params...) + newParams = append(newParams, lambda.Parameters...) + lambda.Parameters = newParams + // Return lambda followed by remaining expressions + result := make([]ast.Expression, 0, len(exprs)-j) + result = append(result, lambda) + result = append(result, exprs[j+1:]...) + return result + } + } + } + + // No merge needed return exprs } @@ -1019,6 +1071,13 @@ func (p *Parser) parseGroupedOrTuple() ast.Expression { binExpr.Parenthesized = true } + // Mark lambda expressions as parenthesized so we don't merge them + // with preceding identifiers in multi-param lambda detection + // e.g., f(a, (x -> y)) should NOT merge 'a' into the lambda + if lambda, ok := first.(*ast.Lambda); ok { + lambda.Parenthesized = true + } + return first } diff --git a/parser/testdata/02418_aggregate_combinators/metadata.json b/parser/testdata/02418_aggregate_combinators/metadata.json index 63a23fba1d..0967ef424b 100644 --- a/parser/testdata/02418_aggregate_combinators/metadata.json +++ b/parser/testdata/02418_aggregate_combinators/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt16": true, - "stmt28": true - } -} +{} diff --git a/parser/testdata/02718_array_fold/metadata.json b/parser/testdata/02718_array_fold/metadata.json index 5a38f68bcb..0967ef424b 100644 --- a/parser/testdata/02718_array_fold/metadata.json +++ b/parser/testdata/02718_array_fold/metadata.json @@ -1,21 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt20": true, - "stmt21": true, - "stmt22": true, - "stmt27": true, - "stmt34": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} From 0a10b2c49a524a6efcfaec82010119fed749459f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:27:15 +0000 Subject: [PATCH 17/22] Add UNDROP TABLE statement support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for parsing and explaining UNDROP TABLE statements: - UNDROP TABLE name - UNDROP TABLE db.name - UNDROP TABLE name ON CLUSTER cluster - UNDROP TABLE name UUID 'uuid' Fixes 7 statements in 02681_undrop_query test (16→9 pending). --- ast/ast.go | 13 ++++++ internal/explain/explain.go | 2 + internal/explain/statements.go | 15 ++++++ parser/parser.go | 46 +++++++++++++++++++ .../testdata/02681_undrop_query/metadata.json | 9 +--- token/token.go | 2 + 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index d8e77b81bb..d54d2ff0e2 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -499,6 +499,19 @@ func (d *DropQuery) Pos() token.Position { return d.Position } func (d *DropQuery) End() token.Position { return d.Position } func (d *DropQuery) statementNode() {} +// UndropQuery represents an UNDROP TABLE statement. +type UndropQuery struct { + Position token.Position `json:"-"` + Database string `json:"database,omitempty"` + Table string `json:"table"` + OnCluster string `json:"on_cluster,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +func (u *UndropQuery) Pos() token.Position { return u.Position } +func (u *UndropQuery) End() token.Position { return u.Position } +func (u *UndropQuery) statementNode() {} + // AlterQuery represents an ALTER statement. type AlterQuery struct { Position token.Position `json:"-"` diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 1c052a68d6..561fdb6329 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -113,6 +113,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) { explainCreateQuery(sb, n, indent, depth) case *ast.DropQuery: explainDropQuery(sb, n, indent, depth) + case *ast.UndropQuery: + explainUndropQuery(sb, n, indent, depth) case *ast.RenameQuery: explainRenameQuery(sb, n, indent, depth) case *ast.ExchangeQuery: diff --git a/internal/explain/statements.go b/internal/explain/statements.go index a84cc3a1fd..273d7cd098 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -505,6 +505,21 @@ func explainDropQuery(sb *strings.Builder, n *ast.DropQuery, indent string, dept } } +func explainUndropQuery(sb *strings.Builder, n *ast.UndropQuery, indent string, depth int) { + name := n.Table + // Check if we have a database-qualified name (for UNDROP TABLE db.table) + hasDatabase := n.Database != "" + if hasDatabase { + // Database-qualified: UndropQuery db table (children 2) + fmt.Fprintf(sb, "%sUndropQuery %s %s (children %d)\n", indent, EscapeIdentifier(n.Database), EscapeIdentifier(name), 2) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(n.Database)) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) + } else { + fmt.Fprintf(sb, "%sUndropQuery %s (children %d)\n", indent, EscapeIdentifier(name), 1) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) + } +} + func explainRenameQuery(sb *strings.Builder, n *ast.RenameQuery, indent string, depth int) { if n == nil { fmt.Fprintf(sb, "%s*ast.RenameQuery\n", indent) diff --git a/parser/parser.go b/parser/parser.go index b410331299..954a5381fb 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -180,6 +180,8 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseAlter() case token.TRUNCATE: return p.parseTruncate() + case token.UNDROP: + return p.parseUndrop() case token.USE: return p.parseUse() case token.DESCRIBE, token.DESC: @@ -4446,6 +4448,50 @@ func (p *Parser) parseTruncate() *ast.TruncateQuery { return trunc } +func (p *Parser) parseUndrop() *ast.UndropQuery { + undrop := &ast.UndropQuery{ + Position: p.current.Pos, + } + + p.nextToken() // skip UNDROP + + if p.currentIs(token.TABLE) { + p.nextToken() + } + + // Parse table name (can start with a number in ClickHouse) + tableName := p.parseIdentifierName() + if tableName != "" { + if p.currentIs(token.DOT) { + p.nextToken() + undrop.Database = tableName + undrop.Table = p.parseIdentifierName() + } else { + undrop.Table = tableName + } + } + + // Handle ON CLUSTER + if p.currentIs(token.ON) { + p.nextToken() + if p.currentIs(token.CLUSTER) { + p.nextToken() + undrop.OnCluster = p.parseIdentifierName() + } + } + + // Handle UUID + if p.currentIs(token.IDENT) && strings.ToUpper(p.current.Value) == "UUID" { + p.nextToken() + if p.currentIs(token.STRING) { + undrop.UUID = p.current.Value + p.nextToken() + } + } + + return undrop +} + func (p *Parser) parseUse() *ast.UseQuery { use := &ast.UseQuery{ Position: p.current.Pos, diff --git a/parser/testdata/02681_undrop_query/metadata.json b/parser/testdata/02681_undrop_query/metadata.json index 2919bbb280..cb6306de40 100644 --- a/parser/testdata/02681_undrop_query/metadata.json +++ b/parser/testdata/02681_undrop_query/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt16": true, "stmt22": true, "stmt23": true, "stmt25": true, @@ -9,12 +8,6 @@ "stmt32": true, "stmt34": true, "stmt36": true, - "stmt38": true, - "stmt45": true, - "stmt54": true, - "stmt62": true, - "stmt76": true, - "stmt78": true, - "stmt8": true + "stmt38": true } } diff --git a/token/token.go b/token/token.go index 8704587dc8..05bf17cbe7 100644 --- a/token/token.go +++ b/token/token.go @@ -191,6 +191,7 @@ const ( TRUE TRUNCATE TTL + UNDROP UNION UPDATE USE @@ -387,6 +388,7 @@ var tokens = [...]string{ TRUE: "TRUE", TRUNCATE: "TRUNCATE", TTL: "TTL", + UNDROP: "UNDROP", UNION: "UNION", UPDATE: "UPDATE", USE: "USE", From d1edb0d7fcb26fdffb399edec554f4e71b23dded Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:32:42 +0000 Subject: [PATCH 18/22] Allow NULL values in IN clause tuple literal formatting When formatting IN clauses like `i in (1, 3, NULL)`, the list can now be combined into a compact tuple literal format when it contains a mix of: - Numeric values + NULLs - String values + NULLs Previously, the presence of NULL would force the verbose Function tuple format. Now `in (1, 3, NULL)` correctly outputs `Literal Tuple_(UInt64_1, UInt64_3, NULL)`. Fixes all 16 statements in 01231_operator_null_in and many statements in: - 00441_nulls_in - 00939_test_null_in - 01410_nullable_key_and_index - 01507_transform_null_in - 01558_transform_null_in - 01756_optimize_skip_unused_shards_rewrite_in - 02499_analyzer_aggregate_function_lambda_crash_fix - 03234_evaluate_constant_analyzer - 03393_non_constant_second_argument_for_in - 03578_kv_in_type_casts --- internal/explain/functions.go | 50 ++++++++++++------- parser/testdata/00441_nulls_in/metadata.json | 6 +-- .../testdata/00939_test_null_in/metadata.json | 6 +-- .../01231_operator_null_in/metadata.json | 21 +------- .../metadata.json | 6 +-- .../01507_transform_null_in/metadata.json | 8 +-- .../01558_transform_null_in/metadata.json | 11 +--- .../metadata.json | 1 - .../metadata.json | 7 +-- .../metadata.json | 6 +-- .../metadata.json | 2 - .../03578_kv_in_type_casts/metadata.json | 7 +-- 12 files changed, 41 insertions(+), 90 deletions(-) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index b03c3792fb..7a5a6e998a 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -659,22 +659,28 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int) // Determine if the IN list should be combined into a single tuple literal // This happens when we have multiple literals of compatible types: - // - All numeric literals/expressions (integers/floats, including unary minus) - // - All string literals + // - All numeric literals/expressions (integers/floats, including unary minus) + NULLs + // - All string literals + NULLs // - All tuple literals that contain only primitive literals (recursively) canBeTupleLiteral := false if n.Query == nil && len(n.List) > 1 { - allNumeric := true - allStrings := true + allNumericOrNull := true + allStringsOrNull := true allTuples := true allTuplesArePrimitive := true + hasNonNull := false // Need at least one non-null value for _, item := range n.List { if lit, ok := item.(*ast.Literal); ok { + if lit.Type == ast.LiteralNull { + // NULL is compatible with both numeric and string lists + continue + } + hasNonNull = true if lit.Type != ast.LiteralInteger && lit.Type != ast.LiteralFloat { - allNumeric = false + allNumericOrNull = false } if lit.Type != ast.LiteralString { - allStrings = false + allStringsOrNull = false } if lit.Type != ast.LiteralTuple { allTuples = false @@ -686,17 +692,18 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int) } } else if isNumericExpr(item) { // Unary minus of numeric is still numeric - allStrings = false + hasNonNull = true + allStringsOrNull = false allTuples = false } else { - allNumeric = false - allStrings = false + allNumericOrNull = false + allStringsOrNull = false allTuples = false break } } // For tuples, only combine if all contain primitive literals - canBeTupleLiteral = allNumeric || allStrings || (allTuples && allTuplesArePrimitive) + canBeTupleLiteral = hasNonNull && (allNumericOrNull || allStringsOrNull || (allTuples && allTuplesArePrimitive)) } // Count arguments: expr + list items or subquery @@ -814,17 +821,23 @@ func explainInExprWithAlias(sb *strings.Builder, n *ast.InExpr, alias string, in const maxStringTupleSizeWithAlias = 10 canBeTupleLiteral := false if n.Query == nil && len(n.List) > 1 { - allNumeric := true - allStrings := true + allNumericOrNull := true + allStringsOrNull := true allTuples := true allTuplesArePrimitive := true + hasNonNull := false // Need at least one non-null value for _, item := range n.List { if lit, ok := item.(*ast.Literal); ok { + if lit.Type == ast.LiteralNull { + // NULL is compatible with both numeric and string lists + continue + } + hasNonNull = true if lit.Type != ast.LiteralInteger && lit.Type != ast.LiteralFloat { - allNumeric = false + allNumericOrNull = false } if lit.Type != ast.LiteralString { - allStrings = false + allStringsOrNull = false } if lit.Type != ast.LiteralTuple { allTuples = false @@ -834,16 +847,17 @@ func explainInExprWithAlias(sb *strings.Builder, n *ast.InExpr, alias string, in } } } else if isNumericExpr(item) { - allStrings = false + hasNonNull = true + allStringsOrNull = false allTuples = false } else { - allNumeric = false - allStrings = false + allNumericOrNull = false + allStringsOrNull = false allTuples = false break } } - canBeTupleLiteral = allNumeric || (allStrings && len(n.List) <= maxStringTupleSizeWithAlias) || (allTuples && allTuplesArePrimitive) + canBeTupleLiteral = hasNonNull && (allNumericOrNull || (allStringsOrNull && len(n.List) <= maxStringTupleSizeWithAlias) || (allTuples && allTuplesArePrimitive)) } // Count arguments diff --git a/parser/testdata/00441_nulls_in/metadata.json b/parser/testdata/00441_nulls_in/metadata.json index 5c1dd64ff1..9409bfce8e 100644 --- a/parser/testdata/00441_nulls_in/metadata.json +++ b/parser/testdata/00441_nulls_in/metadata.json @@ -1,13 +1,9 @@ { "explain_todo": { - "stmt1": true, "stmt13": true, "stmt14": true, "stmt15": true, "stmt16": true, - "stmt17": true, - "stmt2": true, - "stmt7": true, - "stmt8": true + "stmt17": true } } diff --git a/parser/testdata/00939_test_null_in/metadata.json b/parser/testdata/00939_test_null_in/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/00939_test_null_in/metadata.json +++ b/parser/testdata/00939_test_null_in/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/01231_operator_null_in/metadata.json b/parser/testdata/01231_operator_null_in/metadata.json index dd58de52fb..0967ef424b 100644 --- a/parser/testdata/01231_operator_null_in/metadata.json +++ b/parser/testdata/01231_operator_null_in/metadata.json @@ -1,20 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt12": true, - "stmt13": true, - "stmt15": true, - "stmt17": true, - "stmt19": true, - "stmt20": true, - "stmt22": true, - "stmt23": true, - "stmt25": true, - "stmt26": true, - "stmt28": true, - "stmt4": true, - "stmt6": true, - "stmt7": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/01410_nullable_key_and_index/metadata.json b/parser/testdata/01410_nullable_key_and_index/metadata.json index b330691357..0967ef424b 100644 --- a/parser/testdata/01410_nullable_key_and_index/metadata.json +++ b/parser/testdata/01410_nullable_key_and_index/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt26": true - } -} +{} diff --git a/parser/testdata/01507_transform_null_in/metadata.json b/parser/testdata/01507_transform_null_in/metadata.json index 4f7d5e224d..0967ef424b 100644 --- a/parser/testdata/01507_transform_null_in/metadata.json +++ b/parser/testdata/01507_transform_null_in/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt4": true, - "stmt6": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/01558_transform_null_in/metadata.json b/parser/testdata/01558_transform_null_in/metadata.json index 93db919f01..a08759fb21 100644 --- a/parser/testdata/01558_transform_null_in/metadata.json +++ b/parser/testdata/01558_transform_null_in/metadata.json @@ -1,14 +1,5 @@ { "explain_todo": { - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt21": true, - "stmt5": true, - "stmt6": true, - "stmt9": true + "stmt21": true } } diff --git a/parser/testdata/01756_optimize_skip_unused_shards_rewrite_in/metadata.json b/parser/testdata/01756_optimize_skip_unused_shards_rewrite_in/metadata.json index 224a6c1b3e..3a1aa49b68 100644 --- a/parser/testdata/01756_optimize_skip_unused_shards_rewrite_in/metadata.json +++ b/parser/testdata/01756_optimize_skip_unused_shards_rewrite_in/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt45": true, "stmt46": true, - "stmt53": true, "stmt56": true, "stmt57": true } diff --git a/parser/testdata/02499_analyzer_aggregate_function_lambda_crash_fix/metadata.json b/parser/testdata/02499_analyzer_aggregate_function_lambda_crash_fix/metadata.json index bc141058a4..0967ef424b 100644 --- a/parser/testdata/02499_analyzer_aggregate_function_lambda_crash_fix/metadata.json +++ b/parser/testdata/02499_analyzer_aggregate_function_lambda_crash_fix/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt2": true, - "stmt3": true - } -} +{} diff --git a/parser/testdata/03234_evaluate_constant_analyzer/metadata.json b/parser/testdata/03234_evaluate_constant_analyzer/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/03234_evaluate_constant_analyzer/metadata.json +++ b/parser/testdata/03234_evaluate_constant_analyzer/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json b/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json index 1e53d6353e..c89ada73f6 100644 --- a/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json +++ b/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt30": true, - "stmt31": true, "stmt43": true, "stmt44": true, "stmt45": true diff --git a/parser/testdata/03578_kv_in_type_casts/metadata.json b/parser/testdata/03578_kv_in_type_casts/metadata.json index 1c89544edc..0967ef424b 100644 --- a/parser/testdata/03578_kv_in_type_casts/metadata.json +++ b/parser/testdata/03578_kv_in_type_casts/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt32": true, - "stmt73": true - } -} +{} From 947e6a96441df8efd6375e1e276f8d16a1e66ed0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:41:41 +0000 Subject: [PATCH 19/22] Add lambda support for APPLY transformer on * and COLUMNS() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for lambda expressions in APPLY transformers: - `* APPLY x -> toString(x)` - `COLUMNS(...) APPLY x -> expr` Previously only simple function names were supported (e.g., `* APPLY toString`). This adds: - ApplyLambda field to ast.ColumnTransformer struct - Lambda detection and parsing in parseAsteriskApply and parseColumnsApply Fixes 14 statements in 02378_analyzer_projection_names (16→2 pending) and improves several other tests. --- ast/ast.go | 11 ++++---- parser/expression.go | 26 ++++++++++++++++--- .../01470_columns_transformers2/metadata.json | 4 +-- .../metadata.json | 11 +------- .../02343_analyzer_lambdas/metadata.json | 8 +----- .../metadata.json | 16 +----------- .../metadata.json | 8 +----- .../03144_invalid_filter/metadata.json | 6 +---- .../metadata.json | 6 +---- 9 files changed, 35 insertions(+), 61 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index d54d2ff0e2..7330196296 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -1215,11 +1215,12 @@ func (r *ReplaceExpr) End() token.Position { return r.Position } // ColumnTransformer represents a single transformer (APPLY, EXCEPT, or REPLACE) in order. type ColumnTransformer struct { - Position token.Position `json:"-"` - Type string `json:"type"` // "apply", "except", "replace" - Apply string `json:"apply,omitempty"` // function name for APPLY - Except []string `json:"except,omitempty"` // column names for EXCEPT - Replaces []*ReplaceExpr `json:"replaces,omitempty"` // replacement expressions for REPLACE + Position token.Position `json:"-"` + Type string `json:"type"` // "apply", "except", "replace" + Apply string `json:"apply,omitempty"` // function name for APPLY + ApplyLambda Expression `json:"apply_lambda,omitempty"` // lambda expression for APPLY x -> expr + Except []string `json:"except,omitempty"` // column names for EXCEPT + Replaces []*ReplaceExpr `json:"replaces,omitempty"` // replacement expressions for REPLACE } // ColumnsMatcher represents COLUMNS('pattern') or COLUMNS(col1, col2) expression. diff --git a/parser/expression.go b/parser/expression.go index 9e07a65e68..d761d30e59 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -2557,8 +2557,17 @@ func (p *Parser) parseAsteriskApply(asterisk *ast.Asterisk) ast.Expression { p.nextToken() // skip ( } - // Parse function name (can be IDENT or keyword like sum, avg, etc.) - if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + // Check for lambda expression: x -> expr + if p.currentIs(token.IDENT) && p.peekIs(token.ARROW) { + // Parse lambda expression + lambda := p.parseExpression(LOWEST) + asterisk.Transformers = append(asterisk.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "apply", + ApplyLambda: lambda, + }) + } else if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + // Parse function name (can be IDENT or keyword like sum, avg, etc.) funcName := p.current.Value asterisk.Apply = append(asterisk.Apply, funcName) asterisk.Transformers = append(asterisk.Transformers, &ast.ColumnTransformer{ @@ -2586,8 +2595,17 @@ func (p *Parser) parseColumnsApply(matcher *ast.ColumnsMatcher) ast.Expression { p.nextToken() // skip ( } - // Parse function name (can be IDENT or keyword like sum, avg, etc.) - if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + // Check for lambda expression: x -> expr + if p.currentIs(token.IDENT) && p.peekIs(token.ARROW) { + // Parse lambda expression + lambda := p.parseExpression(LOWEST) + matcher.Transformers = append(matcher.Transformers, &ast.ColumnTransformer{ + Position: pos, + Type: "apply", + ApplyLambda: lambda, + }) + } else if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + // Parse function name (can be IDENT or keyword like sum, avg, etc.) funcName := p.current.Value matcher.Apply = append(matcher.Apply, funcName) matcher.Transformers = append(matcher.Transformers, &ast.ColumnTransformer{ diff --git a/parser/testdata/01470_columns_transformers2/metadata.json b/parser/testdata/01470_columns_transformers2/metadata.json index f3773105ca..b65b07d7a6 100644 --- a/parser/testdata/01470_columns_transformers2/metadata.json +++ b/parser/testdata/01470_columns_transformers2/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt4": true, - "stmt6": true, - "stmt7": true + "stmt4": true } } diff --git a/parser/testdata/02339_analyzer_matcher_basic/metadata.json b/parser/testdata/02339_analyzer_matcher_basic/metadata.json index 9372bbb5fc..0967ef424b 100644 --- a/parser/testdata/02339_analyzer_matcher_basic/metadata.json +++ b/parser/testdata/02339_analyzer_matcher_basic/metadata.json @@ -1,10 +1 @@ -{ - "explain_todo": { - "stmt63": true, - "stmt64": true, - "stmt66": true, - "stmt67": true, - "stmt69": true, - "stmt70": true - } -} +{} diff --git a/parser/testdata/02343_analyzer_lambdas/metadata.json b/parser/testdata/02343_analyzer_lambdas/metadata.json index 06afe672c2..0967ef424b 100644 --- a/parser/testdata/02343_analyzer_lambdas/metadata.json +++ b/parser/testdata/02343_analyzer_lambdas/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt31": true, - "stmt32": true, - "stmt33": true - } -} +{} diff --git a/parser/testdata/02378_analyzer_projection_names/metadata.json b/parser/testdata/02378_analyzer_projection_names/metadata.json index f40cb4ffb8..6256fd41ef 100644 --- a/parser/testdata/02378_analyzer_projection_names/metadata.json +++ b/parser/testdata/02378_analyzer_projection_names/metadata.json @@ -1,20 +1,6 @@ { "explain_todo": { - "stmt101": true, - "stmt105": true, - "stmt109": true, "stmt185": true, - "stmt207": true, - "stmt215": true, - "stmt227": true, - "stmt235": true, - "stmt33": true, - "stmt37": true, - "stmt41": true, - "stmt57": true, - "stmt67": true, - "stmt89": true, - "stmt93": true, - "stmt97": true + "stmt67": true } } diff --git a/parser/testdata/02493_analyzer_table_functions_untuple/metadata.json b/parser/testdata/02493_analyzer_table_functions_untuple/metadata.json index 7888b3f2fc..0967ef424b 100644 --- a/parser/testdata/02493_analyzer_table_functions_untuple/metadata.json +++ b/parser/testdata/02493_analyzer_table_functions_untuple/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt20": true, - "stmt22": true, - "stmt24": true - } -} +{} diff --git a/parser/testdata/03144_invalid_filter/metadata.json b/parser/testdata/03144_invalid_filter/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/03144_invalid_filter/metadata.json +++ b/parser/testdata/03144_invalid_filter/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} diff --git a/parser/testdata/03212_optimize_with_constraints_logical_error/metadata.json b/parser/testdata/03212_optimize_with_constraints_logical_error/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/03212_optimize_with_constraints_logical_error/metadata.json +++ b/parser/testdata/03212_optimize_with_constraints_logical_error/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} From da40d74833989774b0aaf5d616fdae655c8b205b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:51:20 +0000 Subject: [PATCH 20/22] Add plural interval unit normalization (years -> Year) Handle both plural interval forms like INTERVAL 2 years and string literal intervals like INTERVAL '2 years' by normalizing the unit name and extracting value/unit from strings. Changes: - Add normalizeIntervalUnit() to strip trailing 's' and title-case - Update explainIntervalExpr() to parse string literal intervals - Update explainDateAddSubResult() to use normalizeIntervalUnit() Fixes 28 statements across 6 tests including 02884_interval_operator_support_plural_literal. --- internal/explain/functions.go | 60 ++++++++++++++++--- .../metadata.json | 9 +-- .../metadata.json | 7 +-- .../metadata.json | 20 +------ .../metadata.json | 3 +- .../metadata.json | 4 +- .../03593_any_join_swap_tables/metadata.json | 6 +- 7 files changed, 59 insertions(+), 50 deletions(-) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 7a5a6e998a..4627effdd5 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -7,6 +7,21 @@ import ( "github.com/sqlc-dev/doubleclick/ast" ) +// normalizeIntervalUnit converts interval units to title-cased singular form +// e.g., "years" -> "Year", "MONTH" -> "Month", "days" -> "Day" +func normalizeIntervalUnit(unit string) string { + if len(unit) == 0 { + return "" + } + u := strings.ToLower(unit) + // Remove trailing 's' for plural forms + if strings.HasSuffix(u, "s") && len(u) > 1 { + u = u[:len(u)-1] + } + // Title-case + return strings.ToUpper(u[:1]) + u[1:] +} + func explainFunctionCall(sb *strings.Builder, n *ast.FunctionCall, indent string, depth int) { explainFunctionCallWithAlias(sb, n, n.Alias, indent, depth) } @@ -199,8 +214,8 @@ func explainDateAddSubResult(sb *strings.Builder, opFunc string, dateArg, valueA Node(sb, dateArg, depth+2) // Second arg: toIntervalUnit(value) - unitTitled := strings.ToUpper(unit[:1]) + strings.ToLower(unit[1:]) - fmt.Fprintf(sb, "%s Function toInterval%s (children %d)\n", indent, unitTitled, 1) + unitNorm := normalizeIntervalUnit(unit) + fmt.Fprintf(sb, "%s Function toInterval%s (children %d)\n", indent, unitNorm, 1) fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) Node(sb, valueArg, depth+4) } @@ -1107,19 +1122,50 @@ func explainCaseExprWithAlias(sb *strings.Builder, n *ast.CaseExpr, alias string func explainIntervalExpr(sb *strings.Builder, n *ast.IntervalExpr, alias string, indent string, depth int) { // INTERVAL is represented as Function toInterval - // Unit needs to be title-cased (e.g., YEAR -> Year) + // Unit needs to be title-cased and singular (e.g., YEAR -> Year, YEARS -> Year) unit := n.Unit - if len(unit) > 0 { - unit = strings.ToUpper(unit[:1]) + strings.ToLower(unit[1:]) + value := n.Value + + // Handle string literals like INTERVAL '2 years' - extract value and unit + if unit == "" { + if lit, ok := n.Value.(*ast.Literal); ok && lit.Type == ast.LiteralString { + if strVal, ok := lit.Value.(string); ok { + val, u := parseIntervalString(strVal) + if u != "" { + unit = u + // Create a numeric literal for the value + value = &ast.Literal{ + Type: ast.LiteralInteger, + Value: val, + } + } + } + } } - fnName := "toInterval" + unit + + unitNorm := normalizeIntervalUnit(unit) + fnName := "toInterval" + unitNorm if alias != "" { fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, alias, 1) } else { fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1) } fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) - Node(sb, n.Value, depth+2) + Node(sb, value, depth+2) +} + +// parseIntervalString parses a string like "2 years" into value and unit +func parseIntervalString(s string) (value string, unit string) { + // Trim surrounding quotes if present + s = strings.Trim(s, "'\"") + s = strings.TrimSpace(s) + + // Find the split between number and unit + parts := strings.Fields(s) + if len(parts) >= 2 { + return parts[0], parts[1] + } + return s, "" } func explainExistsExpr(sb *strings.Builder, n *ast.ExistsExpr, indent string, depth int) { diff --git a/parser/testdata/01523_interval_operator_support_string_literal/metadata.json b/parser/testdata/01523_interval_operator_support_string_literal/metadata.json index e572034190..8c6a18d871 100644 --- a/parser/testdata/01523_interval_operator_support_string_literal/metadata.json +++ b/parser/testdata/01523_interval_operator_support_string_literal/metadata.json @@ -1,12 +1,5 @@ { "explain_todo": { - "stmt12": true, - "stmt15": true, - "stmt18": true, - "stmt19": true, - "stmt24": true, - "stmt3": true, - "stmt6": true, - "stmt9": true + "stmt19": true } } diff --git a/parser/testdata/02735_system_zookeeper_connection/metadata.json b/parser/testdata/02735_system_zookeeper_connection/metadata.json index ef382ce51e..0967ef424b 100644 --- a/parser/testdata/02735_system_zookeeper_connection/metadata.json +++ b/parser/testdata/02735_system_zookeeper_connection/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt4": true, - "stmt5": true - } -} +{} diff --git a/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json b/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json index 790fda8db5..0967ef424b 100644 --- a/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json +++ b/parser/testdata/02884_interval_operator_support_plural_literal/metadata.json @@ -1,19 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt12": true, - "stmt14": true, - "stmt15": true, - "stmt17": true, - "stmt18": true, - "stmt2": true, - "stmt20": true, - "stmt22": true, - "stmt23": true, - "stmt3": true, - "stmt5": true, - "stmt6": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03011_definitive_guide_to_cast/metadata.json b/parser/testdata/03011_definitive_guide_to_cast/metadata.json index 90500807fc..4320920101 100644 --- a/parser/testdata/03011_definitive_guide_to_cast/metadata.json +++ b/parser/testdata/03011_definitive_guide_to_cast/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt36": true, "stmt53": true, - "stmt84": true, - "stmt86": true + "stmt84": true } } diff --git a/parser/testdata/03254_last_2_samples_aggregate_function_simple/metadata.json b/parser/testdata/03254_last_2_samples_aggregate_function_simple/metadata.json index a74c293460..b65b07d7a6 100644 --- a/parser/testdata/03254_last_2_samples_aggregate_function_simple/metadata.json +++ b/parser/testdata/03254_last_2_samples_aggregate_function_simple/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt4": true, - "stmt8": true, - "stmt9": true + "stmt4": true } } diff --git a/parser/testdata/03593_any_join_swap_tables/metadata.json b/parser/testdata/03593_any_join_swap_tables/metadata.json index 7ad5569408..0967ef424b 100644 --- a/parser/testdata/03593_any_join_swap_tables/metadata.json +++ b/parser/testdata/03593_any_join_swap_tables/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt9": true - } -} +{} From 141903c4b94fa49f72bd665be8dcbd693c3adf28 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 17:57:30 +0000 Subject: [PATCH 21/22] Handle INTERVAL '2' AS alias unit syntax in parser Support for aliases on the value expression in INTERVAL syntax, like INTERVAL '2' AS n minute. Fixes 01523_interval_operator_support_string_literal (now all 25 pass). --- parser/expression.go | 10 ++++++++++ .../metadata.json | 6 +----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/parser/expression.go b/parser/expression.go index d761d30e59..1320ebefc5 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -1444,6 +1444,16 @@ func (p *Parser) parseInterval() ast.Expression { // Use ALIAS_PREC to prevent consuming the unit as an alias expr.Value = p.parseExpression(ALIAS_PREC) + // Handle INTERVAL '2' AS n minute - where AS n is alias on the value + if p.currentIs(token.AS) { + p.nextToken() // skip AS + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + alias := p.current.Value + p.nextToken() + expr.Value = p.wrapWithAlias(expr.Value, alias) + } + } + // Parse unit (interval units are identifiers like DAY, MONTH, etc.) if p.currentIs(token.IDENT) { expr.Unit = strings.ToUpper(p.current.Value) diff --git a/parser/testdata/01523_interval_operator_support_string_literal/metadata.json b/parser/testdata/01523_interval_operator_support_string_literal/metadata.json index 8c6a18d871..0967ef424b 100644 --- a/parser/testdata/01523_interval_operator_support_string_literal/metadata.json +++ b/parser/testdata/01523_interval_operator_support_string_literal/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt19": true - } -} +{} From 4c5028252078cb921546253ea785a471547eb673 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 18:02:21 +0000 Subject: [PATCH 22/22] Fix NULL::Type cast to keep Literal NULL format When casting NULL with :: operator syntax (e.g. NULL::Nullable(UInt8)), output as Literal NULL rather than Literal \'NULL\'. Fixes many statements across 27 tests. --- internal/explain/functions.go | 3 +++ .../00255_array_concat_string/metadata.json | 9 ++++++++- parser/testdata/00727_concat/metadata.json | 9 +-------- .../metadata.json | 6 +----- .../02277_full_sort_join_misc/metadata.json | 6 +----- .../metadata.json | 8 +------- .../metadata.json | 2 +- parser/testdata/02797_range_nullable/metadata.json | 2 +- parser/testdata/02809_has_subsequence/metadata.json | 2 +- .../metadata.json | 13 +------------ .../02922_respect_nulls_extensive/metadata.json | 7 +------ .../02922_respect_nulls_states/metadata.json | 2 +- .../02935_format_with_arbitrary_types/metadata.json | 9 +-------- parser/testdata/02942_variant_cast/metadata.json | 2 +- parser/testdata/02943_variant_element/metadata.json | 8 +------- .../metadata.json | 13 +------------ .../02990_arrayFold_nullable_lc/metadata.json | 7 +------ .../03032_string_to_variant_cast/metadata.json | 8 +------- .../testdata/03210_dynamic_squashing/metadata.json | 7 +------ .../metadata.json | 10 +--------- .../03408_hash_functions_on_null/metadata.json | 13 +------------ .../metadata.json | 4 +--- .../metadata.json | 4 +--- .../03522_function_first_non_default/metadata.json | 12 +----------- .../metadata.json | 8 +------- .../03606_nullable_json_group_by/metadata.json | 6 +----- .../03639_hash_of_dynamic_column/metadata.json | 2 +- .../03707_function_array_remove/metadata.json | 7 +------ 28 files changed, 37 insertions(+), 152 deletions(-) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 4627effdd5..2ca9ed3475 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -336,6 +336,9 @@ func explainCastExprWithAlias(sb *strings.Builder, n *ast.CastExpr, alias string exprStr := formatExprAsString(lit) fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, exprStr) } + } else if lit.Type == ast.LiteralNull { + // NULL stays as Literal NULL, not formatted as a string + fmt.Fprintf(sb, "%s Literal NULL\n", indent) } else { // Simple literal - format as string exprStr := formatExprAsString(lit) diff --git a/parser/testdata/00255_array_concat_string/metadata.json b/parser/testdata/00255_array_concat_string/metadata.json index 3030f42b2c..4a3785ad92 100644 --- a/parser/testdata/00255_array_concat_string/metadata.json +++ b/parser/testdata/00255_array_concat_string/metadata.json @@ -1 +1,8 @@ -{"explain_todo":{"stmt12":true,"stmt15":true,"stmt16":true,"stmt17":true,"stmt18":true,"stmt20":true}} +{ + "explain_todo": { + "stmt15": true, + "stmt16": true, + "stmt17": true, + "stmt18": true + } +} diff --git a/parser/testdata/00727_concat/metadata.json b/parser/testdata/00727_concat/metadata.json index 36ee3e64c6..898e7ce13d 100644 --- a/parser/testdata/00727_concat/metadata.json +++ b/parser/testdata/00727_concat/metadata.json @@ -3,13 +3,6 @@ "stmt19": true, "stmt20": true, "stmt44": true, - "stmt46": true, - "stmt62": true, - "stmt63": true, - "stmt64": true, - "stmt65": true, - "stmt66": true, - "stmt67": true, - "stmt83": true + "stmt46": true } } diff --git a/parser/testdata/01410_nullable_key_and_index_negate_cond/metadata.json b/parser/testdata/01410_nullable_key_and_index_negate_cond/metadata.json index 983800a6c0..0967ef424b 100644 --- a/parser/testdata/01410_nullable_key_and_index_negate_cond/metadata.json +++ b/parser/testdata/01410_nullable_key_and_index_negate_cond/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt23": true - } -} +{} diff --git a/parser/testdata/02277_full_sort_join_misc/metadata.json b/parser/testdata/02277_full_sort_join_misc/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/02277_full_sort_join_misc/metadata.json +++ b/parser/testdata/02277_full_sort_join_misc/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} diff --git a/parser/testdata/02302_defaults_in_columnar_formats/metadata.json b/parser/testdata/02302_defaults_in_columnar_formats/metadata.json index d6ab88c49c..0967ef424b 100644 --- a/parser/testdata/02302_defaults_in_columnar_formats/metadata.json +++ b/parser/testdata/02302_defaults_in_columnar_formats/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt3": true, - "stmt5": true - } -} +{} diff --git a/parser/testdata/02320_mapped_array_witn_const_nullable/metadata.json b/parser/testdata/02320_mapped_array_witn_const_nullable/metadata.json index b7645733e4..0967ef424b 100644 --- a/parser/testdata/02320_mapped_array_witn_const_nullable/metadata.json +++ b/parser/testdata/02320_mapped_array_witn_const_nullable/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt5":true,"stmt6":true}} +{} diff --git a/parser/testdata/02797_range_nullable/metadata.json b/parser/testdata/02797_range_nullable/metadata.json index 0cdd4a17b5..0967ef424b 100644 --- a/parser/testdata/02797_range_nullable/metadata.json +++ b/parser/testdata/02797_range_nullable/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt10":true,"stmt11":true,"stmt12":true,"stmt9":true}} +{} diff --git a/parser/testdata/02809_has_subsequence/metadata.json b/parser/testdata/02809_has_subsequence/metadata.json index f894d0f2de..0967ef424b 100644 --- a/parser/testdata/02809_has_subsequence/metadata.json +++ b/parser/testdata/02809_has_subsequence/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt61":true}} +{} diff --git a/parser/testdata/02922_analyzer_aggregate_nothing_type/metadata.json b/parser/testdata/02922_analyzer_aggregate_nothing_type/metadata.json index 30dca0857e..0967ef424b 100644 --- a/parser/testdata/02922_analyzer_aggregate_nothing_type/metadata.json +++ b/parser/testdata/02922_analyzer_aggregate_nothing_type/metadata.json @@ -1,12 +1 @@ -{ - "explain_todo": { - "stmt27": true, - "stmt28": true, - "stmt29": true, - "stmt3": true, - "stmt30": true, - "stmt4": true, - "stmt7": true, - "stmt82": true - } -} +{} diff --git a/parser/testdata/02922_respect_nulls_extensive/metadata.json b/parser/testdata/02922_respect_nulls_extensive/metadata.json index d689cec729..0967ef424b 100644 --- a/parser/testdata/02922_respect_nulls_extensive/metadata.json +++ b/parser/testdata/02922_respect_nulls_extensive/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt40": true, - "stmt41": true - } -} +{} diff --git a/parser/testdata/02922_respect_nulls_states/metadata.json b/parser/testdata/02922_respect_nulls_states/metadata.json index 02a89fff19..0967ef424b 100644 --- a/parser/testdata/02922_respect_nulls_states/metadata.json +++ b/parser/testdata/02922_respect_nulls_states/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt3":true,"stmt5":true,"stmt7":true,"stmt9":true}} +{} diff --git a/parser/testdata/02935_format_with_arbitrary_types/metadata.json b/parser/testdata/02935_format_with_arbitrary_types/metadata.json index 4024074f48..898e7ce13d 100644 --- a/parser/testdata/02935_format_with_arbitrary_types/metadata.json +++ b/parser/testdata/02935_format_with_arbitrary_types/metadata.json @@ -3,13 +3,6 @@ "stmt19": true, "stmt20": true, "stmt44": true, - "stmt46": true, - "stmt55": true, - "stmt56": true, - "stmt57": true, - "stmt58": true, - "stmt59": true, - "stmt60": true, - "stmt76": true + "stmt46": true } } diff --git a/parser/testdata/02942_variant_cast/metadata.json b/parser/testdata/02942_variant_cast/metadata.json index 63554d20f0..0967ef424b 100644 --- a/parser/testdata/02942_variant_cast/metadata.json +++ b/parser/testdata/02942_variant_cast/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt12":true,"stmt13":true,"stmt16":true,"stmt17":true,"stmt2":true}} +{} diff --git a/parser/testdata/02943_variant_element/metadata.json b/parser/testdata/02943_variant_element/metadata.json index 4a52a8e22d..0967ef424b 100644 --- a/parser/testdata/02943_variant_element/metadata.json +++ b/parser/testdata/02943_variant_element/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt3": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/02944_variant_as_common_type_analyzer/metadata.json b/parser/testdata/02944_variant_as_common_type_analyzer/metadata.json index d25aa33927..0967ef424b 100644 --- a/parser/testdata/02944_variant_as_common_type_analyzer/metadata.json +++ b/parser/testdata/02944_variant_as_common_type_analyzer/metadata.json @@ -1,12 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt18": true, - "stmt19": true, - "stmt26": true, - "stmt27": true, - "stmt34": true, - "stmt35": true - } -} +{} diff --git a/parser/testdata/02990_arrayFold_nullable_lc/metadata.json b/parser/testdata/02990_arrayFold_nullable_lc/metadata.json index af67e43fa8..0967ef424b 100644 --- a/parser/testdata/02990_arrayFold_nullable_lc/metadata.json +++ b/parser/testdata/02990_arrayFold_nullable_lc/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt24": true, - "stmt25": true - } -} +{} diff --git a/parser/testdata/03032_string_to_variant_cast/metadata.json b/parser/testdata/03032_string_to_variant_cast/metadata.json index cbba02e8b3..0967ef424b 100644 --- a/parser/testdata/03032_string_to_variant_cast/metadata.json +++ b/parser/testdata/03032_string_to_variant_cast/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt14": true, - "stmt15": true - } -} +{} diff --git a/parser/testdata/03210_dynamic_squashing/metadata.json b/parser/testdata/03210_dynamic_squashing/metadata.json index a954a15222..0967ef424b 100644 --- a/parser/testdata/03210_dynamic_squashing/metadata.json +++ b/parser/testdata/03210_dynamic_squashing/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt5": true - } -} +{} diff --git a/parser/testdata/03261_any_respect_camelCase_aliases/metadata.json b/parser/testdata/03261_any_respect_camelCase_aliases/metadata.json index 31f6f1e5e0..0967ef424b 100644 --- a/parser/testdata/03261_any_respect_camelCase_aliases/metadata.json +++ b/parser/testdata/03261_any_respect_camelCase_aliases/metadata.json @@ -1,9 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt16": true, - "stmt22": true, - "stmt28": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03408_hash_functions_on_null/metadata.json b/parser/testdata/03408_hash_functions_on_null/metadata.json index 8fc71f1a4f..0967ef424b 100644 --- a/parser/testdata/03408_hash_functions_on_null/metadata.json +++ b/parser/testdata/03408_hash_functions_on_null/metadata.json @@ -1,12 +1 @@ -{ - "explain_todo": { - "stmt11": true, - "stmt12": true, - "stmt18": true, - "stmt19": true, - "stmt23": true, - "stmt24": true, - "stmt6": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_aggregating_merge_tree/metadata.json b/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_aggregating_merge_tree/metadata.json index a4f8773303..b65b07d7a6 100644 --- a/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_aggregating_merge_tree/metadata.json +++ b/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_aggregating_merge_tree/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt3": true, - "stmt4": true, - "stmt5": true + "stmt4": true } } diff --git a/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_summing_merge_tree/metadata.json b/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_summing_merge_tree/metadata.json index a4f8773303..b65b07d7a6 100644 --- a/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_summing_merge_tree/metadata.json +++ b/parser/testdata/03513_simple_aggregate_function_any_respect_nulls_in_summing_merge_tree/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt3": true, - "stmt4": true, - "stmt5": true + "stmt4": true } } diff --git a/parser/testdata/03522_function_first_non_default/metadata.json b/parser/testdata/03522_function_first_non_default/metadata.json index 568bfbc472..0967ef424b 100644 --- a/parser/testdata/03522_function_first_non_default/metadata.json +++ b/parser/testdata/03522_function_first_non_default/metadata.json @@ -1,11 +1 @@ -{ - "explain_todo": { - "stmt16": true, - "stmt2": true, - "stmt22": true, - "stmt23": true, - "stmt33": true, - "stmt4": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/03525_transform_null_in_subqeury_with_not_nullable_type/metadata.json b/parser/testdata/03525_transform_null_in_subqeury_with_not_nullable_type/metadata.json index fffcb7d38b..0967ef424b 100644 --- a/parser/testdata/03525_transform_null_in_subqeury_with_not_nullable_type/metadata.json +++ b/parser/testdata/03525_transform_null_in_subqeury_with_not_nullable_type/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt2": true, - "stmt3": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03606_nullable_json_group_by/metadata.json b/parser/testdata/03606_nullable_json_group_by/metadata.json index 1295a45747..0967ef424b 100644 --- a/parser/testdata/03606_nullable_json_group_by/metadata.json +++ b/parser/testdata/03606_nullable_json_group_by/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt3": true - } -} +{} diff --git a/parser/testdata/03639_hash_of_dynamic_column/metadata.json b/parser/testdata/03639_hash_of_dynamic_column/metadata.json index d109968791..0967ef424b 100644 --- a/parser/testdata/03639_hash_of_dynamic_column/metadata.json +++ b/parser/testdata/03639_hash_of_dynamic_column/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt10":true,"stmt9":true}} +{} diff --git a/parser/testdata/03707_function_array_remove/metadata.json b/parser/testdata/03707_function_array_remove/metadata.json index 7ee47c55de..0967ef424b 100644 --- a/parser/testdata/03707_function_array_remove/metadata.json +++ b/parser/testdata/03707_function_array_remove/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt45": true, - "stmt46": true - } -} +{}