Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ type CheckQuery struct {
Position token.Position `json:"-"`
Database string `json:"database,omitempty"`
Table string `json:"table"`
Format string `json:"format,omitempty"`
Settings []*SettingExpr `json:"settings,omitempty"`
}

Expand Down
2 changes: 1 addition & 1 deletion internal/explain/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func Node(sb *strings.Builder, node interface{}, depth int) {
case *ast.AlterQuery:
explainAlterQuery(sb, n, indent, depth)
case *ast.OptimizeQuery:
explainOptimizeQuery(sb, n, indent)
explainOptimizeQuery(sb, n, indent, depth)
case *ast.TruncateQuery:
explainTruncateQuery(sb, n, indent)
case *ast.CheckQuery:
Expand Down
102 changes: 67 additions & 35 deletions internal/explain/statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
}
}
}
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 {
// For materialized views, output AsSelect before storage definition
if n.Materialized && n.AsSelect != nil {
Node(sb, n.AsSelect, depth+1)
}
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
if hasStorage {
storageChildren := 0
if n.Engine != nil {
storageChildren++
Expand Down Expand Up @@ -231,80 +236,91 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
if len(n.Settings) > 0 {
storageChildren++
}
fmt.Fprintf(sb, "%s Storage definition (children %d)\n", indent, storageChildren)
// For materialized views, wrap storage definition in ViewTargets
// and use extra indentation for storage children
storageIndent := indent + " " // 1 space for regular storage (format strings add 1 more)
storageChildDepth := depth + 2
if n.Materialized {
fmt.Fprintf(sb, "%s ViewTargets (children %d)\n", indent, 1)
fmt.Fprintf(sb, "%s Storage definition (children %d)\n", indent, storageChildren)
storageIndent = indent + " " // 2 spaces for materialized (format strings add 1 more = 3 total)
storageChildDepth = depth + 3
} else {
fmt.Fprintf(sb, "%s Storage definition (children %d)\n", indent, storageChildren)
}
if n.Engine != nil {
if n.Engine.HasParentheses {
fmt.Fprintf(sb, "%s Function %s (children %d)\n", indent, n.Engine.Name, 1)
fmt.Fprintf(sb, "%s Function %s (children %d)\n", storageIndent, n.Engine.Name, 1)
if len(n.Engine.Parameters) > 0 {
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Engine.Parameters))
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.Engine.Parameters))
for _, param := range n.Engine.Parameters {
Node(sb, param, depth+4)
Node(sb, param, storageChildDepth+2)
}
} else {
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
fmt.Fprintf(sb, "%s ExpressionList\n", storageIndent)
}
} else {
fmt.Fprintf(sb, "%s Function %s\n", indent, n.Engine.Name)
fmt.Fprintf(sb, "%s Function %s\n", storageIndent, n.Engine.Name)
}
}
if n.PartitionBy != nil {
if ident, ok := n.PartitionBy.(*ast.Identifier); ok {
fmt.Fprintf(sb, "%s Identifier %s\n", indent, ident.Name())
fmt.Fprintf(sb, "%s Identifier %s\n", storageIndent, ident.Name())
} else {
Node(sb, n.PartitionBy, depth+2)
Node(sb, n.PartitionBy, storageChildDepth)
}
}
if len(n.OrderBy) > 0 {
if len(n.OrderBy) == 1 {
if ident, ok := n.OrderBy[0].(*ast.Identifier); ok {
fmt.Fprintf(sb, "%s Identifier %s\n", indent, ident.Name())
fmt.Fprintf(sb, "%s Identifier %s\n", storageIndent, ident.Name())
} else if lit, ok := n.OrderBy[0].(*ast.Literal); ok && lit.Type == ast.LiteralTuple {
// Handle tuple literal (including empty tuple from ORDER BY ())
exprs, _ := lit.Value.([]ast.Expression)
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
if len(exprs) > 0 {
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs))
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(exprs))
for _, e := range exprs {
Node(sb, e, depth+4)
Node(sb, e, storageChildDepth+2)
}
} else {
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
fmt.Fprintf(sb, "%s ExpressionList\n", storageIndent)
}
} else {
Node(sb, n.OrderBy[0], depth+2)
Node(sb, n.OrderBy[0], storageChildDepth)
}
} else {
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.OrderBy))
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.OrderBy))
for _, o := range n.OrderBy {
Node(sb, o, depth+4)
Node(sb, o, storageChildDepth+2)
}
}
}
if len(n.PrimaryKey) > 0 {
if len(n.PrimaryKey) == 1 {
if ident, ok := n.PrimaryKey[0].(*ast.Identifier); ok {
fmt.Fprintf(sb, "%s Identifier %s\n", indent, ident.Name())
fmt.Fprintf(sb, "%s Identifier %s\n", storageIndent, ident.Name())
} else if lit, ok := n.PrimaryKey[0].(*ast.Literal); ok && lit.Type == ast.LiteralTuple {
// Handle tuple literal (including empty tuple from PRIMARY KEY ())
exprs, _ := lit.Value.([]ast.Expression)
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
if len(exprs) > 0 {
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs))
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(exprs))
for _, e := range exprs {
Node(sb, e, depth+4)
Node(sb, e, storageChildDepth+2)
}
} else {
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
fmt.Fprintf(sb, "%s ExpressionList\n", storageIndent)
}
} else {
Node(sb, n.PrimaryKey[0], depth+2)
Node(sb, n.PrimaryKey[0], storageChildDepth)
}
} else {
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.PrimaryKey))
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", storageIndent, 1)
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", storageIndent, len(n.PrimaryKey))
for _, p := range n.PrimaryKey {
Node(sb, p, depth+4)
Node(sb, p, storageChildDepth+2)
}
}
}
Expand All @@ -323,20 +339,21 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string,
}
}
if showSampleBy {
Node(sb, n.SampleBy, depth+2)
Node(sb, n.SampleBy, storageChildDepth)
}
}
}
if n.TTL != nil {
fmt.Fprintf(sb, "%s ExpressionList (children 1)\n", indent)
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", indent)
Node(sb, n.TTL.Expression, depth+4)
fmt.Fprintf(sb, "%s ExpressionList (children 1)\n", storageIndent)
fmt.Fprintf(sb, "%s TTLElement (children 1)\n", storageIndent)
Node(sb, n.TTL.Expression, storageChildDepth+2)
}
if len(n.Settings) > 0 {
fmt.Fprintf(sb, "%s Set\n", indent)
fmt.Fprintf(sb, "%s Set\n", storageIndent)
}
}
if n.AsSelect != nil {
// For non-materialized views, output AsSelect after storage
if n.AsSelect != nil && !n.Materialized {
// AS SELECT is output directly without Subquery wrapper
Node(sb, n.AsSelect, depth+1)
}
Expand Down Expand Up @@ -908,7 +925,7 @@ func countAlterCommandChildren(cmd *ast.AlterCommand) int {
return children
}

func explainOptimizeQuery(sb *strings.Builder, n *ast.OptimizeQuery, indent string) {
func explainOptimizeQuery(sb *strings.Builder, n *ast.OptimizeQuery, indent string, depth int) {
if n == nil {
fmt.Fprintf(sb, "%s*ast.OptimizeQuery\n", indent)
return
Expand All @@ -919,7 +936,16 @@ func explainOptimizeQuery(sb *strings.Builder, n *ast.OptimizeQuery, indent stri
name += "_final"
}

fmt.Fprintf(sb, "%sOptimizeQuery %s (children %d)\n", indent, name, 1)
children := 1 // identifier
if n.Partition != nil {
children++
}

fmt.Fprintf(sb, "%sOptimizeQuery %s (children %d)\n", indent, name, children)
if n.Partition != nil {
fmt.Fprintf(sb, "%s Partition (children 1)\n", indent)
Node(sb, n.Partition, depth+2)
}
fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table)
}

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

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)
}
Expand Down
34 changes: 33 additions & 1 deletion internal/explain/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ func formatSampleRatioOperand(sb *strings.Builder, expr ast.Expression) {
case uint64:
fmt.Fprintf(sb, "%d", v)
case float64:
fmt.Fprintf(sb, "%g", v)
// Convert decimal to fraction for EXPLAIN AST output
// ClickHouse shows 0.1 as "1 / 10", 0.01 as "1 / 100", etc.
if frac := floatToFraction(v); frac != "" {
sb.WriteString(frac)
} else {
fmt.Fprintf(sb, "%g", v)
}
default:
fmt.Fprintf(sb, "%v", v)
}
Expand All @@ -100,6 +106,32 @@ func formatSampleRatioOperand(sb *strings.Builder, expr ast.Expression) {
}
}

// floatToFraction converts a float to a fraction string like "1 / 10"
// Returns empty string if the float can't be reasonably converted to a simple fraction
func floatToFraction(f float64) string {
if f <= 0 || f >= 1 {
return ""
}
// Try common denominators
denominators := []int64{2, 3, 4, 5, 8, 10, 16, 20, 25, 32, 50, 64, 100, 128, 1000, 10000, 100000, 1000000}
for _, denom := range denominators {
num := f * float64(denom)
// Check if num is close to an integer
rounded := int64(num + 0.5)
if rounded > 0 && abs(num-float64(rounded)) < 1e-9 {
return fmt.Sprintf("%d / %d", rounded, denom)
}
}
return ""
}

func abs(x float64) float64 {
if x < 0 {
return -x
}
return x
}

// explainViewExplain handles EXPLAIN queries used as table sources, converting to viewExplain function
func explainViewExplain(sb *strings.Builder, n *ast.ExplainQuery, alias string, indent string, depth int) {
// When EXPLAIN is used as a table source, it becomes viewExplain function
Expand Down
6 changes: 6 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3754,6 +3754,12 @@ func (p *Parser) parseCheck() *ast.CheckQuery {
check.Table = tableName
}

// Parse optional FORMAT
if p.currentIs(token.FORMAT) {
p.nextToken() // skip FORMAT
check.Format = p.parseIdentifierName()
}

// Parse optional SETTINGS
if p.currentIs(token.SETTINGS) {
p.nextToken() // skip SETTINGS
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"explain_todo": {
"stmt3": true
}
}
{}
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"explain_todo": {
"stmt5": true
}
}
{}
2 changes: 1 addition & 1 deletion parser/testdata/00056_view/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"explain_todo":{"stmt6":true}}
{}
6 changes: 1 addition & 5 deletions parser/testdata/00063_check_query/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"explain_todo": {
"stmt9": true
}
}
{}
6 changes: 1 addition & 5 deletions parser/testdata/00084_summing_merge_tree/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"explain_todo": {
"stmt25": true
}
}
{}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"explain_todo": {
"stmt27": true,
"stmt30": true,
"stmt35": true,
"stmt36": true,
"stmt37": true,
Expand Down
6 changes: 1 addition & 5 deletions parser/testdata/00180_attach_materialized_view/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"explain_todo": {
"stmt5": true
}
}
{}
4 changes: 1 addition & 3 deletions parser/testdata/00276_sample/metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"explain_todo": {
"stmt13": true,
"stmt14": true,
"stmt15": true,
"stmt16": true,
Expand All @@ -15,7 +14,6 @@
"stmt32": true,
"stmt33": true,
"stmt34": true,
"stmt39": true,
"stmt9": true
"stmt39": true
}
}
6 changes: 1 addition & 5 deletions parser/testdata/00327_summing_composite_nested/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"explain_todo": {
"stmt13": true
}
}
{}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
"stmt38": true,
"stmt43": true,
"stmt44": true,
"stmt46": true,
"stmt47": true,
"stmt48": true,
"stmt8": true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
"explain_todo": {
"stmt13": true,
"stmt17": true,
"stmt26": true,
"stmt42": true,
"stmt43": true,
"stmt48": true,
"stmt59": true,
"stmt8": true
"stmt48": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@
"explain_todo": {
"stmt12": true,
"stmt13": true,
"stmt14": true,
"stmt19": true,
"stmt33": true,
"stmt34": true,
"stmt35": true,
"stmt54": true,
"stmt55": true,
"stmt56": true,
"stmt57": true,
"stmt62": true,
"stmt76": true,
"stmt77": true,
"stmt78": true,
"stmt94": true,
"stmt95": true,
"stmt96": true
"stmt95": true
}
}
Loading