diff --git a/ast/ast.go b/ast/ast.go index 7ac6d0d42..0b7cb822f 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -70,8 +70,10 @@ type SelectQuery struct { Qualify Expression `json:"qualify,omitempty"` Window []*WindowDefinition `json:"window,omitempty"` OrderBy []*OrderByElement `json:"order_by,omitempty"` - Limit Expression `json:"limit,omitempty"` - Offset Expression `json:"offset,omitempty"` + Limit Expression `json:"limit,omitempty"` + LimitBy []Expression `json:"limit_by,omitempty"` + LimitByHasLimit bool `json:"limit_by_has_limit,omitempty"` // true if LIMIT BY was followed by another LIMIT + Offset Expression `json:"offset,omitempty"` Settings []*SettingExpr `json:"settings,omitempty"` IntoOutfile *IntoOutfileClause `json:"into_outfile,omitempty"` Format *Identifier `json:"format,omitempty"` @@ -1067,11 +1069,12 @@ func (i *IsNullExpr) expressionNode() {} // LikeExpr represents a LIKE or ILIKE expression. type LikeExpr struct { - Position token.Position `json:"-"` - Expr Expression `json:"expr"` - Not bool `json:"not,omitempty"` - CaseInsensitive bool `json:"case_insensitive,omitempty"` // true for ILIKE - Pattern Expression `json:"pattern"` + Position token.Position `json:"-"` + Expr Expression `json:"expr"` + Not bool `json:"not,omitempty"` + CaseInsensitive bool `json:"case_insensitive,omitempty"` // true for ILIKE + Pattern Expression `json:"pattern"` + Alias string `json:"alias,omitempty"` } func (l *LikeExpr) Pos() token.Position { return l.Position } diff --git a/internal/explain/format.go b/internal/explain/format.go index b58d8bf6d..c75976705 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -21,12 +21,16 @@ func FormatFloat(val float64) string { if math.IsNaN(val) { return "nan" } - // Use scientific notation for extremely small numbers (< 1e-10) - // This matches ClickHouse's behavior where numbers like 0.000001 stay decimal - // but extremely small numbers like 1e-38 use scientific notation + // Use scientific notation for very small numbers (< 1e-6) + // This matches ClickHouse's behavior where numbers like 0.0000001 (-1e-7) + // are displayed in scientific notation absVal := math.Abs(val) - if absVal > 0 && absVal < 1e-10 { - return strconv.FormatFloat(val, 'e', -1, 64) + if absVal > 0 && absVal < 1e-6 { + s := strconv.FormatFloat(val, 'e', -1, 64) + // Remove leading zeros from exponent (e-07 -> e-7) + s = strings.Replace(s, "e-0", "e-", 1) + s = strings.Replace(s, "e+0", "e+", 1) + return s } // Use decimal notation for normal-sized numbers return strconv.FormatFloat(val, 'f', -1, 64) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 0095192b5..84b143983 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -655,7 +655,11 @@ func explainLikeExpr(sb *strings.Builder, n *ast.LikeExpr, indent string, depth if n.Not { fnName = "not" + strings.Title(fnName) } - fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1) + if n.Alias != "" { + fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1) + } fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2) Node(sb, n.Expr, depth+2) Node(sb, n.Pattern, depth+2) diff --git a/internal/explain/select.go b/internal/explain/select.go index dd5013af3..c90ad34e5 100644 --- a/internal/explain/select.go +++ b/internal/explain/select.go @@ -113,6 +113,13 @@ func explainSelectQuery(sb *strings.Builder, n *ast.SelectQuery, indent string, if n.Limit != nil { Node(sb, n.Limit, depth+1) } + // LIMIT BY - only output when there's no ORDER BY and no second LIMIT (matches ClickHouse behavior) + if len(n.LimitBy) > 0 && len(n.OrderBy) == 0 && !n.LimitByHasLimit { + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.LimitBy)) + for _, expr := range n.LimitBy { + Node(sb, expr, depth+2) + } + } // SETTINGS - output here if there's no FORMAT, otherwise it's at SelectWithUnionQuery level if len(n.Settings) > 0 && n.Format == nil { fmt.Fprintf(sb, "%s Set\n", indent) @@ -195,6 +202,16 @@ func isComplexExpr(expr ast.Expression) bool { } } +// hasOnlyLiterals checks if all expressions in a slice are literals +func hasOnlyLiterals(exprs []ast.Expression) bool { + for _, expr := range exprs { + if _, ok := expr.(*ast.Literal); !ok { + return false + } + } + return true +} + func countSelectUnionChildren(n *ast.SelectWithUnionQuery) int { count := 1 // ExpressionList of selects // Check if any SelectQuery has IntoOutfile set @@ -259,6 +276,9 @@ func countSelectQueryChildren(n *ast.SelectQuery) int { if n.Limit != nil { count++ } + if len(n.LimitBy) > 0 && len(n.OrderBy) == 0 && !n.LimitByHasLimit { + count++ + } if n.Offset != nil { count++ } diff --git a/parser/expression.go b/parser/expression.go index a7e977a50..ccc3e284d 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -419,6 +419,33 @@ func (p *Parser) parseIdentifierOrFunction() ast.Expression { name := p.current.Value p.nextToken() + // Check for MySQL-style @@variable syntax (system variables) + // Convert to globalVariable('varname') function call with alias @@varname + if strings.HasPrefix(name, "@@") { + varName := name[2:] // Strip @@ + // Handle @@session.var or @@global.var + if p.currentIs(token.DOT) { + p.nextToken() + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + varName = varName + "." + p.current.Value + name = name + "." + p.current.Value + p.nextToken() + } + } + return &ast.FunctionCall{ + Position: pos, + Name: "globalVariable", + Alias: name, + Arguments: []ast.Expression{ + &ast.Literal{ + Position: pos, + Type: "String", + Value: varName, + }, + }, + } + } + // Check for function call if p.currentIs(token.LPAREN) { return p.parseFunctionCall(name, pos) @@ -1591,6 +1618,9 @@ func (p *Parser) parseAlias(left ast.Expression) ast.Expression { case *ast.ExtractExpr: e.Alias = alias return e + case *ast.LikeExpr: + e.Alias = alias + return e default: return &ast.AliasedExpr{ Position: left.Pos(), diff --git a/parser/parser.go b/parser/parser.go index d4bf52a7d..4a70be3f6 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -444,9 +444,10 @@ func (p *Parser) parseSelect() *ast.SelectQuery { // LIMIT BY clause (ClickHouse specific: LIMIT n BY expr1, expr2, ...) if p.currentIs(token.BY) { p.nextToken() - // Parse LIMIT BY expressions - skip them for now + // Parse LIMIT BY expressions for !p.isEndOfExpression() { - p.parseExpression(LOWEST) + expr := p.parseExpression(LOWEST) + sel.LimitBy = append(sel.LimitBy, expr) if p.currentIs(token.COMMA) { p.nextToken() } else { @@ -457,6 +458,7 @@ func (p *Parser) parseSelect() *ast.SelectQuery { if p.currentIs(token.LIMIT) { p.nextToken() sel.Limit = p.parseExpression(LOWEST) + sel.LimitByHasLimit = true } } diff --git a/parser/testdata/00176_distinct_limit_by_limit_bug_43377/ast.json b/parser/testdata/00176_distinct_limit_by_limit_bug_43377/ast.json index 837bc3eaa..73613a267 100644 --- a/parser/testdata/00176_distinct_limit_by_limit_bug_43377/ast.json +++ b/parser/testdata/00176_distinct_limit_by_limit_bug_43377/ast.json @@ -94,7 +94,15 @@ "limit": { "type": "Integer", "value": 10 - } + }, + "limit_by": [ + { + "parts": [ + "Title" + ] + } + ], + "limit_by_has_limit": true } ] } diff --git a/parser/testdata/00583_limit_by_expressions/metadata.json b/parser/testdata/00583_limit_by_expressions/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/00583_limit_by_expressions/metadata.json +++ b/parser/testdata/00583_limit_by_expressions/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/00590_limit_by_column_removal/metadata.json b/parser/testdata/00590_limit_by_column_removal/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/00590_limit_by_column_removal/metadata.json +++ b/parser/testdata/00590_limit_by_column_removal/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/01337_mysql_global_variables/metadata.json b/parser/testdata/01337_mysql_global_variables/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/01337_mysql_global_variables/metadata.json +++ b/parser/testdata/01337_mysql_global_variables/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/02045_like_function/metadata.json b/parser/testdata/02045_like_function/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/02045_like_function/metadata.json +++ b/parser/testdata/02045_like_function/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/02281_limit_by_distributed/ast.json b/parser/testdata/02281_limit_by_distributed/ast.json index 66ca711d7..2b656970e 100644 --- a/parser/testdata/02281_limit_by_distributed/ast.json +++ b/parser/testdata/02281_limit_by_distributed/ast.json @@ -98,7 +98,14 @@ "limit": { "type": "Integer", "value": 1 - } + }, + "limit_by": [ + { + "parts": [ + "k" + ] + } + ] } ] } diff --git a/parser/testdata/03213_rand_dos/metadata.json b/parser/testdata/03213_rand_dos/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/03213_rand_dos/metadata.json +++ b/parser/testdata/03213_rand_dos/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/03366_with_fill_dag/ast.json b/parser/testdata/03366_with_fill_dag/ast.json index 63ddbc923..2da1c9c80 100644 --- a/parser/testdata/03366_with_fill_dag/ast.json +++ b/parser/testdata/03366_with_fill_dag/ast.json @@ -48,7 +48,14 @@ "limit": { "type": "Integer", "value": 1 - } + }, + "limit_by": [ + { + "parts": [ + "number" + ] + } + ] } ] }