Skip to content

Commit ab22e1a

Browse files
authored
Fix 5 parser tests with LIMIT BY, MySQL @@ vars, and LIKE alias support (#45)
1 parent 9a6c47b commit ab22e1a

File tree

14 files changed

+108
-23
lines changed

14 files changed

+108
-23
lines changed

ast/ast.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ type SelectQuery struct {
7070
Qualify Expression `json:"qualify,omitempty"`
7171
Window []*WindowDefinition `json:"window,omitempty"`
7272
OrderBy []*OrderByElement `json:"order_by,omitempty"`
73-
Limit Expression `json:"limit,omitempty"`
74-
Offset Expression `json:"offset,omitempty"`
73+
Limit Expression `json:"limit,omitempty"`
74+
LimitBy []Expression `json:"limit_by,omitempty"`
75+
LimitByHasLimit bool `json:"limit_by_has_limit,omitempty"` // true if LIMIT BY was followed by another LIMIT
76+
Offset Expression `json:"offset,omitempty"`
7577
Settings []*SettingExpr `json:"settings,omitempty"`
7678
IntoOutfile *IntoOutfileClause `json:"into_outfile,omitempty"`
7779
Format *Identifier `json:"format,omitempty"`
@@ -1067,11 +1069,12 @@ func (i *IsNullExpr) expressionNode() {}
10671069

10681070
// LikeExpr represents a LIKE or ILIKE expression.
10691071
type LikeExpr struct {
1070-
Position token.Position `json:"-"`
1071-
Expr Expression `json:"expr"`
1072-
Not bool `json:"not,omitempty"`
1073-
CaseInsensitive bool `json:"case_insensitive,omitempty"` // true for ILIKE
1074-
Pattern Expression `json:"pattern"`
1072+
Position token.Position `json:"-"`
1073+
Expr Expression `json:"expr"`
1074+
Not bool `json:"not,omitempty"`
1075+
CaseInsensitive bool `json:"case_insensitive,omitempty"` // true for ILIKE
1076+
Pattern Expression `json:"pattern"`
1077+
Alias string `json:"alias,omitempty"`
10751078
}
10761079

10771080
func (l *LikeExpr) Pos() token.Position { return l.Position }

internal/explain/format.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ func FormatFloat(val float64) string {
2121
if math.IsNaN(val) {
2222
return "nan"
2323
}
24-
// Use scientific notation for extremely small numbers (< 1e-10)
25-
// This matches ClickHouse's behavior where numbers like 0.000001 stay decimal
26-
// but extremely small numbers like 1e-38 use scientific notation
24+
// Use scientific notation for very small numbers (< 1e-6)
25+
// This matches ClickHouse's behavior where numbers like 0.0000001 (-1e-7)
26+
// are displayed in scientific notation
2727
absVal := math.Abs(val)
28-
if absVal > 0 && absVal < 1e-10 {
29-
return strconv.FormatFloat(val, 'e', -1, 64)
28+
if absVal > 0 && absVal < 1e-6 {
29+
s := strconv.FormatFloat(val, 'e', -1, 64)
30+
// Remove leading zeros from exponent (e-07 -> e-7)
31+
s = strings.Replace(s, "e-0", "e-", 1)
32+
s = strings.Replace(s, "e+0", "e+", 1)
33+
return s
3034
}
3135
// Use decimal notation for normal-sized numbers
3236
return strconv.FormatFloat(val, 'f', -1, 64)

internal/explain/functions.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,11 @@ func explainLikeExpr(sb *strings.Builder, n *ast.LikeExpr, indent string, depth
655655
if n.Not {
656656
fnName = "not" + strings.Title(fnName)
657657
}
658-
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
658+
if n.Alias != "" {
659+
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Alias, 1)
660+
} else {
661+
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
662+
}
659663
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2)
660664
Node(sb, n.Expr, depth+2)
661665
Node(sb, n.Pattern, depth+2)

internal/explain/select.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ func explainSelectQuery(sb *strings.Builder, n *ast.SelectQuery, indent string,
113113
if n.Limit != nil {
114114
Node(sb, n.Limit, depth+1)
115115
}
116+
// LIMIT BY - only output when there's no ORDER BY and no second LIMIT (matches ClickHouse behavior)
117+
if len(n.LimitBy) > 0 && len(n.OrderBy) == 0 && !n.LimitByHasLimit {
118+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.LimitBy))
119+
for _, expr := range n.LimitBy {
120+
Node(sb, expr, depth+2)
121+
}
122+
}
116123
// SETTINGS - output here if there's no FORMAT, otherwise it's at SelectWithUnionQuery level
117124
if len(n.Settings) > 0 && n.Format == nil {
118125
fmt.Fprintf(sb, "%s Set\n", indent)
@@ -195,6 +202,16 @@ func isComplexExpr(expr ast.Expression) bool {
195202
}
196203
}
197204

205+
// hasOnlyLiterals checks if all expressions in a slice are literals
206+
func hasOnlyLiterals(exprs []ast.Expression) bool {
207+
for _, expr := range exprs {
208+
if _, ok := expr.(*ast.Literal); !ok {
209+
return false
210+
}
211+
}
212+
return true
213+
}
214+
198215
func countSelectUnionChildren(n *ast.SelectWithUnionQuery) int {
199216
count := 1 // ExpressionList of selects
200217
// Check if any SelectQuery has IntoOutfile set
@@ -259,6 +276,9 @@ func countSelectQueryChildren(n *ast.SelectQuery) int {
259276
if n.Limit != nil {
260277
count++
261278
}
279+
if len(n.LimitBy) > 0 && len(n.OrderBy) == 0 && !n.LimitByHasLimit {
280+
count++
281+
}
262282
if n.Offset != nil {
263283
count++
264284
}

parser/expression.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,33 @@ func (p *Parser) parseIdentifierOrFunction() ast.Expression {
419419
name := p.current.Value
420420
p.nextToken()
421421

422+
// Check for MySQL-style @@variable syntax (system variables)
423+
// Convert to globalVariable('varname') function call with alias @@varname
424+
if strings.HasPrefix(name, "@@") {
425+
varName := name[2:] // Strip @@
426+
// Handle @@session.var or @@global.var
427+
if p.currentIs(token.DOT) {
428+
p.nextToken()
429+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
430+
varName = varName + "." + p.current.Value
431+
name = name + "." + p.current.Value
432+
p.nextToken()
433+
}
434+
}
435+
return &ast.FunctionCall{
436+
Position: pos,
437+
Name: "globalVariable",
438+
Alias: name,
439+
Arguments: []ast.Expression{
440+
&ast.Literal{
441+
Position: pos,
442+
Type: "String",
443+
Value: varName,
444+
},
445+
},
446+
}
447+
}
448+
422449
// Check for function call
423450
if p.currentIs(token.LPAREN) {
424451
return p.parseFunctionCall(name, pos)
@@ -1591,6 +1618,9 @@ func (p *Parser) parseAlias(left ast.Expression) ast.Expression {
15911618
case *ast.ExtractExpr:
15921619
e.Alias = alias
15931620
return e
1621+
case *ast.LikeExpr:
1622+
e.Alias = alias
1623+
return e
15941624
default:
15951625
return &ast.AliasedExpr{
15961626
Position: left.Pos(),

parser/parser.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,9 +444,10 @@ func (p *Parser) parseSelect() *ast.SelectQuery {
444444
// LIMIT BY clause (ClickHouse specific: LIMIT n BY expr1, expr2, ...)
445445
if p.currentIs(token.BY) {
446446
p.nextToken()
447-
// Parse LIMIT BY expressions - skip them for now
447+
// Parse LIMIT BY expressions
448448
for !p.isEndOfExpression() {
449-
p.parseExpression(LOWEST)
449+
expr := p.parseExpression(LOWEST)
450+
sel.LimitBy = append(sel.LimitBy, expr)
450451
if p.currentIs(token.COMMA) {
451452
p.nextToken()
452453
} else {
@@ -457,6 +458,7 @@ func (p *Parser) parseSelect() *ast.SelectQuery {
457458
if p.currentIs(token.LIMIT) {
458459
p.nextToken()
459460
sel.Limit = p.parseExpression(LOWEST)
461+
sel.LimitByHasLimit = true
460462
}
461463
}
462464

parser/testdata/00176_distinct_limit_by_limit_bug_43377/ast.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,15 @@
9494
"limit": {
9595
"type": "Integer",
9696
"value": 10
97-
}
97+
},
98+
"limit_by": [
99+
{
100+
"parts": [
101+
"Title"
102+
]
103+
}
104+
],
105+
"limit_by_has_limit": true
98106
}
99107
]
100108
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)