Skip to content

Commit a196c75

Browse files
committed
Fix multiple EXPLAIN output formatting issues
This commit fixes several EXPLAIN AST output issues: 1. SampleRatio formatting: Convert decimal ratios like 0.1 to fractions like 1/10 to match ClickHouse EXPLAIN AST output 2. CHECK TABLE FORMAT clause: Add support for parsing and outputting the FORMAT clause in CHECK TABLE statements 3. Materialized view ViewTargets: For CREATE MATERIALIZED VIEW statements, output the AS SELECT clause before the storage definition and wrap storage definition in ViewTargets 4. OPTIMIZE TABLE PARTITION: Add support for outputting the PARTITION clause in OPTIMIZE TABLE statements These fixes resolve 44 test statements across many test files.
1 parent 37c13c5 commit a196c75

49 files changed

Lines changed: 148 additions & 205 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ast/ast.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ type CheckQuery struct {
720720
Position token.Position `json:"-"`
721721
Database string `json:"database,omitempty"`
722722
Table string `json:"table"`
723+
Format string `json:"format,omitempty"`
723724
Settings []*SettingExpr `json:"settings,omitempty"`
724725
}
725726

internal/explain/explain.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func Node(sb *strings.Builder, node interface{}, depth int) {
144144
case *ast.AlterQuery:
145145
explainAlterQuery(sb, n, indent, depth)
146146
case *ast.OptimizeQuery:
147-
explainOptimizeQuery(sb, n, indent)
147+
explainOptimizeQuery(sb, n, indent, depth)
148148
case *ast.TruncateQuery:
149149
explainTruncateQuery(sb, n, indent)
150150
case *ast.CheckQuery:

internal/explain/statements.go

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
192192
}
193193
}
194194
}
195-
if n.Engine != nil || len(n.OrderBy) > 0 || len(n.PrimaryKey) > 0 || n.PartitionBy != nil || n.SampleBy != nil || n.TTL != nil || len(n.Settings) > 0 {
195+
// For materialized views, output AsSelect before storage definition
196+
if n.Materialized && n.AsSelect != nil {
197+
Node(sb, n.AsSelect, depth+1)
198+
}
199+
hasStorage := n.Engine != nil || len(n.OrderBy) > 0 || len(n.PrimaryKey) > 0 || n.PartitionBy != nil || n.SampleBy != nil || n.TTL != nil || len(n.Settings) > 0
200+
if hasStorage {
196201
storageChildren := 0
197202
if n.Engine != nil {
198203
storageChildren++
@@ -231,80 +236,91 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
231236
if len(n.Settings) > 0 {
232237
storageChildren++
233238
}
234-
fmt.Fprintf(sb, "%s Storage definition (children %d)\n", indent, storageChildren)
239+
// For materialized views, wrap storage definition in ViewTargets
240+
// and use extra indentation for storage children
241+
storageIndent := indent + " " // 1 space for regular storage (format strings add 1 more)
242+
storageChildDepth := depth + 2
243+
if n.Materialized {
244+
fmt.Fprintf(sb, "%s ViewTargets (children %d)\n", indent, 1)
245+
fmt.Fprintf(sb, "%s Storage definition (children %d)\n", indent, storageChildren)
246+
storageIndent = indent + " " // 2 spaces for materialized (format strings add 1 more = 3 total)
247+
storageChildDepth = depth + 3
248+
} else {
249+
fmt.Fprintf(sb, "%s Storage definition (children %d)\n", indent, storageChildren)
250+
}
235251
if n.Engine != nil {
236252
if n.Engine.HasParentheses {
237-
fmt.Fprintf(sb, "%s Function %s (children %d)\n", indent, n.Engine.Name, 1)
253+
fmt.Fprintf(sb, "%s Function %s (children %d)\n", storageIndent, n.Engine.Name, 1)
238254
if len(n.Engine.Parameters) > 0 {
239-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Engine.Parameters))
255+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.Engine.Parameters))
240256
for _, param := range n.Engine.Parameters {
241-
Node(sb, param, depth+4)
257+
Node(sb, param, storageChildDepth+2)
242258
}
243259
} else {
244-
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
260+
fmt.Fprintf(sb, "%s ExpressionList\n", storageIndent)
245261
}
246262
} else {
247-
fmt.Fprintf(sb, "%s Function %s\n", indent, n.Engine.Name)
263+
fmt.Fprintf(sb, "%s Function %s\n", storageIndent, n.Engine.Name)
248264
}
249265
}
250266
if n.PartitionBy != nil {
251267
if ident, ok := n.PartitionBy.(*ast.Identifier); ok {
252-
fmt.Fprintf(sb, "%s Identifier %s\n", indent, ident.Name())
268+
fmt.Fprintf(sb, "%s Identifier %s\n", storageIndent, ident.Name())
253269
} else {
254-
Node(sb, n.PartitionBy, depth+2)
270+
Node(sb, n.PartitionBy, storageChildDepth)
255271
}
256272
}
257273
if len(n.OrderBy) > 0 {
258274
if len(n.OrderBy) == 1 {
259275
if ident, ok := n.OrderBy[0].(*ast.Identifier); ok {
260-
fmt.Fprintf(sb, "%s Identifier %s\n", indent, ident.Name())
276+
fmt.Fprintf(sb, "%s Identifier %s\n", storageIndent, ident.Name())
261277
} else if lit, ok := n.OrderBy[0].(*ast.Literal); ok && lit.Type == ast.LiteralTuple {
262278
// Handle tuple literal (including empty tuple from ORDER BY ())
263279
exprs, _ := lit.Value.([]ast.Expression)
264-
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
280+
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
265281
if len(exprs) > 0 {
266-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs))
282+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(exprs))
267283
for _, e := range exprs {
268-
Node(sb, e, depth+4)
284+
Node(sb, e, storageChildDepth+2)
269285
}
270286
} else {
271-
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
287+
fmt.Fprintf(sb, "%s ExpressionList\n", storageIndent)
272288
}
273289
} else {
274-
Node(sb, n.OrderBy[0], depth+2)
290+
Node(sb, n.OrderBy[0], storageChildDepth)
275291
}
276292
} else {
277-
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
278-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.OrderBy))
293+
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
294+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.OrderBy))
279295
for _, o := range n.OrderBy {
280-
Node(sb, o, depth+4)
296+
Node(sb, o, storageChildDepth+2)
281297
}
282298
}
283299
}
284300
if len(n.PrimaryKey) > 0 {
285301
if len(n.PrimaryKey) == 1 {
286302
if ident, ok := n.PrimaryKey[0].(*ast.Identifier); ok {
287-
fmt.Fprintf(sb, "%s Identifier %s\n", indent, ident.Name())
303+
fmt.Fprintf(sb, "%s Identifier %s\n", storageIndent, ident.Name())
288304
} else if lit, ok := n.PrimaryKey[0].(*ast.Literal); ok && lit.Type == ast.LiteralTuple {
289305
// Handle tuple literal (including empty tuple from PRIMARY KEY ())
290306
exprs, _ := lit.Value.([]ast.Expression)
291-
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
307+
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
292308
if len(exprs) > 0 {
293-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs))
309+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(exprs))
294310
for _, e := range exprs {
295-
Node(sb, e, depth+4)
311+
Node(sb, e, storageChildDepth+2)
296312
}
297313
} else {
298-
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
314+
fmt.Fprintf(sb, "%s ExpressionList\n", storageIndent)
299315
}
300316
} else {
301-
Node(sb, n.PrimaryKey[0], depth+2)
317+
Node(sb, n.PrimaryKey[0], storageChildDepth)
302318
}
303319
} else {
304-
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
305-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.PrimaryKey))
320+
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
321+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.PrimaryKey))
306322
for _, p := range n.PrimaryKey {
307-
Node(sb, p, depth+4)
323+
Node(sb, p, storageChildDepth+2)
308324
}
309325
}
310326
}
@@ -323,20 +339,21 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
323339
}
324340
}
325341
if showSampleBy {
326-
Node(sb, n.SampleBy, depth+2)
342+
Node(sb, n.SampleBy, storageChildDepth)
327343
}
328344
}
329345
}
330346
if n.TTL != nil {
331-
fmt.Fprintf(sb, "%s ExpressionList (children 1)\n", indent)
332-
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", indent)
333-
Node(sb, n.TTL.Expression, depth+4)
347+
fmt.Fprintf(sb, "%s ExpressionList (children 1)\n", storageIndent)
348+
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", storageIndent)
349+
Node(sb, n.TTL.Expression, storageChildDepth+2)
334350
}
335351
if len(n.Settings) > 0 {
336-
fmt.Fprintf(sb, "%s Set\n", indent)
352+
fmt.Fprintf(sb, "%s Set\n", storageIndent)
337353
}
338354
}
339-
if n.AsSelect != nil {
355+
// For non-materialized views, output AsSelect after storage
356+
if n.AsSelect != nil && !n.Materialized {
340357
// AS SELECT is output directly without Subquery wrapper
341358
Node(sb, n.AsSelect, depth+1)
342359
}
@@ -908,7 +925,7 @@ func countAlterCommandChildren(cmd *ast.AlterCommand) int {
908925
return children
909926
}
910927

911-
func explainOptimizeQuery(sb *strings.Builder, n *ast.OptimizeQuery, indent string) {
928+
func explainOptimizeQuery(sb *strings.Builder, n *ast.OptimizeQuery, indent string, depth int) {
912929
if n == nil {
913930
fmt.Fprintf(sb, "%s*ast.OptimizeQuery\n", indent)
914931
return
@@ -919,7 +936,16 @@ func explainOptimizeQuery(sb *strings.Builder, n *ast.OptimizeQuery, indent stri
919936
name += "_final"
920937
}
921938

922-
fmt.Fprintf(sb, "%sOptimizeQuery %s (children %d)\n", indent, name, 1)
939+
children := 1 // identifier
940+
if n.Partition != nil {
941+
children++
942+
}
943+
944+
fmt.Fprintf(sb, "%sOptimizeQuery %s (children %d)\n", indent, name, children)
945+
if n.Partition != nil {
946+
fmt.Fprintf(sb, "%s Partition (children 1)\n", indent)
947+
Node(sb, n.Partition, depth+2)
948+
}
923949
fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table)
924950
}
925951

@@ -950,12 +976,18 @@ func explainCheckQuery(sb *strings.Builder, n *ast.CheckQuery, indent string) {
950976
}
951977

952978
children := 1 // identifier
979+
if n.Format != "" {
980+
children++
981+
}
953982
if len(n.Settings) > 0 {
954983
children++
955984
}
956985

957986
fmt.Fprintf(sb, "%sCheckQuery %s (children %d)\n", indent, name, children)
958987
fmt.Fprintf(sb, "%s Identifier %s\n", indent, name)
988+
if n.Format != "" {
989+
fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format)
990+
}
959991
if len(n.Settings) > 0 {
960992
fmt.Fprintf(sb, "%s Set\n", indent)
961993
}

internal/explain/tables.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ func formatSampleRatioOperand(sb *strings.Builder, expr ast.Expression) {
9191
case uint64:
9292
fmt.Fprintf(sb, "%d", v)
9393
case float64:
94-
fmt.Fprintf(sb, "%g", v)
94+
// Convert decimal to fraction for EXPLAIN AST output
95+
// ClickHouse shows 0.1 as "1 / 10", 0.01 as "1 / 100", etc.
96+
if frac := floatToFraction(v); frac != "" {
97+
sb.WriteString(frac)
98+
} else {
99+
fmt.Fprintf(sb, "%g", v)
100+
}
95101
default:
96102
fmt.Fprintf(sb, "%v", v)
97103
}
@@ -100,6 +106,32 @@ func formatSampleRatioOperand(sb *strings.Builder, expr ast.Expression) {
100106
}
101107
}
102108

109+
// floatToFraction converts a float to a fraction string like "1 / 10"
110+
// Returns empty string if the float can't be reasonably converted to a simple fraction
111+
func floatToFraction(f float64) string {
112+
if f <= 0 || f >= 1 {
113+
return ""
114+
}
115+
// Try common denominators
116+
denominators := []int64{2, 3, 4, 5, 8, 10, 16, 20, 25, 32, 50, 64, 100, 128, 1000, 10000, 100000, 1000000}
117+
for _, denom := range denominators {
118+
num := f * float64(denom)
119+
// Check if num is close to an integer
120+
rounded := int64(num + 0.5)
121+
if rounded > 0 && abs(num-float64(rounded)) < 1e-9 {
122+
return fmt.Sprintf("%d / %d", rounded, denom)
123+
}
124+
}
125+
return ""
126+
}
127+
128+
func abs(x float64) float64 {
129+
if x < 0 {
130+
return -x
131+
}
132+
return x
133+
}
134+
103135
// explainViewExplain handles EXPLAIN queries used as table sources, converting to viewExplain function
104136
func explainViewExplain(sb *strings.Builder, n *ast.ExplainQuery, alias string, indent string, depth int) {
105137
// When EXPLAIN is used as a table source, it becomes viewExplain function

parser/parser.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3754,6 +3754,12 @@ func (p *Parser) parseCheck() *ast.CheckQuery {
37543754
check.Table = tableName
37553755
}
37563756

3757+
// Parse optional FORMAT
3758+
if p.currentIs(token.FORMAT) {
3759+
p.nextToken() // skip FORMAT
3760+
check.Format = p.parseIdentifierName()
3761+
}
3762+
37573763
// Parse optional SETTINGS
37583764
if p.currentIs(token.SETTINGS) {
37593765
p.nextToken() // skip SETTINGS
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt3": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt5": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"explain_todo":{"stmt6":true}}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt9": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt25": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)