Skip to content

Commit 51a4590

Browse files
authored
Merge pull request #22 from VictoriaMetrics/switch-case
implement case expression support
2 parents 8ad925b + c5b8cc3 commit 51a4590

8 files changed

Lines changed: 245 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ Supported highlights:
139139
- string helpers (`SUBSTR`, `CONCAT`, `TRIM`, `REPLACE`, `LOWER`, `UPPER`),
140140
- math (`ABS`, `CEIL`, `FLOOR`, `ROUND`, `LEAST`, `GREATEST`),
141141
- JSON (`JSON_VALUE`),
142+
- `CASE`/`WHEN`,
142143
- and date helpers (`CURRENT_DATE`, `CURRENT_TIMESTAMP`).
143144
- `WHERE` with comparison operators, `BETWEEN`, `IN`, `LIKE`, `IS (NOT) NULL`
144145
- `ORDER BY`, `LIMIT`, `OFFSET`, `DISTINCT`

cmd/sql-to-logsql/web/ui/src/components/docs/Docs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function Docs() {
8282
<p>
8383
<ul className={"list-disc pl-4 pt-2"}>
8484
<li><code>SUBSTR, CONCAT, LOWER, UPPER, TRIM, LTRIM, RTRIM, REPLACE</code></li>
85-
<li><code>LIKE, NOT LIKE, =, !=, &lt;, &gt;, &lt;=, &gt;=, BETWEEN</code></li>
85+
<li><code>CASE/WHEN, LIKE, NOT LIKE, =, !=, &lt;, &gt;, &lt;=, &gt;=, BETWEEN</code></li>
8686
<li><code>+,-, *, /, %, ^</code></li>
8787
<li><code>ABS, GREATEST, LEAST, ROUND, FLOOR, CEIL, POW, LN, EXP</code></li>
8888
<li><code>SUM, COUNT, MAX, MIN, AVG</code></li>

cmd/sql-to-logsql/web/ui/src/components/sql-editor/examples.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ SELECT UPPER(container), total
8787
FROM container_stats
8888
WHERE container IS NOT NULL
8989
ORDER BY total DESC`,
90+
},
91+
{
92+
id: "case",
93+
title: "Case expression",
94+
sql: `SELECT
95+
x.severity,
96+
COUNT(x.severity)
97+
FROM (
98+
SELECT
99+
CASE log.level
100+
WHEN 'dpanic' THEN 'critical'
101+
WHEN 'panic' THEN 'critical'
102+
WHEN 'error' THEN 'high'
103+
WHEN 'warn' THEN 'medium'
104+
WHEN 'info' THEN 'low'
105+
ELSE 'unknown'
106+
END as severity
107+
FROM logs
108+
WHERE log.level IS NOT NULL
109+
) x
110+
GROUP BY x.severity`,
90111
},
91112
{
92113
id: "subqueries",

lib/logsql/select.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,117 @@ func (v *selectTranslatorVisitor) translateMathProjection(expr ast.Expr, alias s
21932193
return pipe, aliasName, nil
21942194
}
21952195

2196+
func (v *selectTranslatorVisitor) translateCaseExpr(expr *ast.CaseExpr, alias string) ([]string, string, error) {
2197+
if expr == nil || len(expr.When) == 0 {
2198+
return nil, "", &TranslationError{
2199+
Code: http.StatusBadRequest,
2200+
Message: "translator: CASE expression requires WHEN clauses",
2201+
}
2202+
}
2203+
2204+
aliasSource := "case"
2205+
if rendered, err := render.Render(expr); err == nil {
2206+
aliasSource = rendered
2207+
}
2208+
aliasName, err := makeProjectionAlias(strings.TrimSpace(alias), "case", aliasSource)
2209+
if err != nil {
2210+
return nil, "", err
2211+
}
2212+
aliasField := formatFieldName(aliasName)
2213+
2214+
tmp := v.cloneForConditionalEvaluation()
2215+
conditions := make([]string, len(expr.When))
2216+
for i, clause := range expr.When {
2217+
condExpr := clause.Condition
2218+
if expr.Operand != nil {
2219+
condExpr = &ast.BinaryExpr{
2220+
Left: expr.Operand,
2221+
Operator: "=",
2222+
Right: clause.Condition,
2223+
}
2224+
}
2225+
cond, err := tmp.translateExpr(condExpr)
2226+
if err != nil {
2227+
return nil, "", err
2228+
}
2229+
conditions[i] = cond
2230+
}
2231+
prePipes := tmp.collectFilterPrefilters()
2232+
cleanup := tmp.collectFilterCleanup()
2233+
2234+
results := make([]string, len(expr.When))
2235+
for i, clause := range expr.When {
2236+
val, err := v.caseResultValue(clause.Result)
2237+
if err != nil {
2238+
return nil, "", err
2239+
}
2240+
results[i] = val
2241+
}
2242+
2243+
elseVal := quoteString("")
2244+
if expr.Else != nil {
2245+
val, err := v.caseResultValue(expr.Else)
2246+
if err != nil {
2247+
return nil, "", err
2248+
}
2249+
elseVal = val
2250+
}
2251+
2252+
pipes := make([]string, 0, len(prePipes)+len(expr.When)+2)
2253+
pipes = append(pipes, prePipes...)
2254+
pipes = append(pipes, fmt.Sprintf("format %s as %s", elseVal, aliasField))
2255+
for i := len(expr.When) - 1; i >= 0; i-- {
2256+
pipes = append(pipes, fmt.Sprintf("format if (%s) %s as %s", conditions[i], results[i], aliasField))
2257+
}
2258+
pipes = append(pipes, cleanup...)
2259+
2260+
return pipes, aliasName, nil
2261+
}
2262+
2263+
func (v *selectTranslatorVisitor) caseResultValue(expr ast.Expr) (string, error) {
2264+
switch e := expr.(type) {
2265+
case *ast.StringLiteral:
2266+
return quoteString(e.Value), nil
2267+
case *ast.NumericLiteral:
2268+
return e.Value, nil
2269+
case *ast.BooleanLiteral:
2270+
if e.Value {
2271+
return "true", nil
2272+
}
2273+
return "false", nil
2274+
case *ast.Identifier:
2275+
field, err := v.rawFieldName(e)
2276+
if err != nil {
2277+
return "", err
2278+
}
2279+
return fmt.Sprintf("\"<%s>\"", escapeFormatPattern(field)), nil
2280+
case *ast.FuncCall:
2281+
if v.aggResults != nil && isAggregateFunction(e) {
2282+
key, err := v.aggregateKeyFromFunc(e)
2283+
if err != nil {
2284+
return "", err
2285+
}
2286+
name, ok := v.aggResults[key]
2287+
if !ok {
2288+
return "", &TranslationError{
2289+
Code: http.StatusBadRequest,
2290+
Message: "translator: unknown aggregate referenced in CASE result",
2291+
}
2292+
}
2293+
return fmt.Sprintf("\"<%s>\"", escapeFormatPattern(formatFieldName(name))), nil
2294+
}
2295+
return "", &TranslationError{
2296+
Code: http.StatusBadRequest,
2297+
Message: fmt.Sprintf("translator: unsupported function %T in CASE result", e),
2298+
}
2299+
default:
2300+
return "", &TranslationError{
2301+
Code: http.StatusBadRequest,
2302+
Message: fmt.Sprintf("translator: unsupported CASE result expression %T", e),
2303+
}
2304+
}
2305+
}
2306+
21962307
func (v *selectTranslatorVisitor) mathExprToString(expr ast.Expr) (string, error) {
21972308
switch e := expr.(type) {
21982309
case *ast.NumericLiteral:
@@ -2550,6 +2661,13 @@ func (v *selectTranslatorVisitor) buildProjectionPipes(columns []ast.SelectItem,
25502661
}
25512662
computedPipes = append(computedPipes, mathPipe)
25522663
fields = append(fields, formatFieldName(aliasName))
2664+
case *ast.CaseExpr:
2665+
pipes, aliasName, err := v.translateCaseExpr(expr, col.Alias)
2666+
if err != nil {
2667+
return nil, nil, err
2668+
}
2669+
computedPipes = append(computedPipes, pipes...)
2670+
fields = append(fields, formatFieldName(aliasName))
25532671
case *ast.StarExpr:
25542672
return nil, nil, &TranslationError{
25552673
Code: http.StatusBadRequest,
@@ -3023,6 +3141,20 @@ func (v *selectTranslatorVisitor) translateIsNullExpr(expr *ast.IsNullExpr) (str
30233141
return field + ":\"\"", nil
30243142
}
30253143

3144+
func (v *selectTranslatorVisitor) cloneForConditionalEvaluation() *selectTranslatorVisitor {
3145+
clone := *v
3146+
clone.result = ""
3147+
clone.err = nil
3148+
clone.pendingLeftFilter = nil
3149+
clone.filterComputations = nil
3150+
clone.filterOrder = nil
3151+
clone.filterDelete = nil
3152+
clone.filterDeleteSet = nil
3153+
clone.aggTempDeletes = nil
3154+
clone.aggPreserve = nil
3155+
return &clone
3156+
}
3157+
30263158
func (v *selectTranslatorVisitor) ensureFilterFunctionAlias(fn *ast.FuncCall) (string, error) {
30273159
if fn == nil || len(fn.Name.Parts) == 0 {
30283160
return "", &TranslationError{

lib/logsql/select_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,22 @@ func TestToLogsQLSuccess(t *testing.T) {
326326
sql: "SELECT REPLACE(message, 'foo', 'bar') AS updated FROM logs",
327327
expected: "* | format \"<message>\" as updated | replace ('foo', 'bar') at updated | fields updated",
328328
},
329+
{
330+
name: "case expression",
331+
sql: `SELECT CASE
332+
WHEN score >= 90 THEN 'critical'
333+
WHEN score >= 75 THEN 'high'
334+
WHEN score >= 50 THEN 'medium'
335+
WHEN score >= 25 THEN 'low'
336+
ELSE 'info'
337+
END AS severity FROM logs`,
338+
expected: "* | format \"info\" as severity | format if (score:>=25) \"low\" as severity | format if (score:>=50) \"medium\" as severity | format if (score:>=75) \"high\" as severity | format if (score:>=90) \"critical\" as severity | fields severity",
339+
},
340+
{
341+
name: "case expression with operand",
342+
sql: "SELECT CASE level WHEN 'error' THEN 'critical' WHEN 'warn' THEN 'warning' ELSE 'info' END AS sev FROM logs",
343+
expected: "* | format \"info\" as sev | format if (level:warn) \"warning\" as sev | format if (level:error) \"critical\" as sev | fields sev",
344+
},
329345
{
330346
name: "json value simple path with alias",
331347
sql: "SELECT JSON_VALUE(payload, '$.ip') AS ip FROM logs",

lib/sql/parser/parser.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,8 @@ func (p *Parser) parseExpression(precedence int) ast.Expr {
738738
left = p.parseQualifiedName()
739739
case token.REPLACE:
740740
left = &ast.Identifier{Parts: []string{p.curToken.Literal}}
741+
case token.CASE:
742+
left = p.parseCaseExpression()
741743
case token.NUMBER:
742744
left = &ast.NumericLiteral{Value: p.curToken.Literal}
743745
case token.STRING:
@@ -790,13 +792,55 @@ func (p *Parser) parseExpression(precedence int) ast.Expr {
790792
func terminatesExpression(t token.Type) bool {
791793
switch t {
792794
case token.SEMICOLON, token.COMMA, token.RPAREN, token.GROUP, token.ORDER, token.LIMIT, token.OFFSET,
793-
token.HAVING, token.UNION, token.INTERSECT, token.EXCEPT:
795+
token.HAVING, token.UNION, token.INTERSECT, token.EXCEPT, token.THEN, token.ELSE, token.END:
794796
return true
795797
default:
796798
return false
797799
}
798800
}
799801

802+
func (p *Parser) parseCaseExpression() ast.Expr {
803+
caseExpr := &ast.CaseExpr{}
804+
805+
if !p.peekTokenIs(token.WHEN) {
806+
p.nextToken()
807+
caseExpr.Operand = p.parseExpression(lowest)
808+
if !p.expectPeek(token.WHEN) {
809+
return caseExpr
810+
}
811+
} else {
812+
p.nextToken()
813+
}
814+
815+
for p.curTokenIs(token.WHEN) {
816+
p.nextToken()
817+
cond := p.parseExpression(lowest)
818+
if !p.expectPeek(token.THEN) {
819+
return caseExpr
820+
}
821+
p.nextToken()
822+
result := p.parseExpression(lowest)
823+
caseExpr.When = append(caseExpr.When, ast.WhenClause{
824+
Condition: cond,
825+
Result: result,
826+
})
827+
if p.peekTokenIs(token.WHEN) {
828+
p.nextToken()
829+
} else {
830+
break
831+
}
832+
}
833+
834+
if p.peekTokenIs(token.ELSE) {
835+
p.nextToken()
836+
p.nextToken()
837+
caseExpr.Else = p.parseExpression(lowest)
838+
}
839+
840+
p.expectPeek(token.END)
841+
return caseExpr
842+
}
843+
800844
func (p *Parser) parseExistsExpression(negate bool) ast.Expr {
801845
if !p.expectPeek(token.LPAREN) {
802846
return nil

lib/sql/render/render.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,25 @@ func (r *renderer) renderExpr(expr ast.Expr) {
418418
r.write("EXISTS (")
419419
r.renderSelect(e.Subquery)
420420
r.write(")")
421+
case *ast.CaseExpr:
422+
r.write("CASE ")
423+
if e.Operand != nil {
424+
r.renderExpr(e.Operand)
425+
r.write(" ")
426+
}
427+
for _, clause := range e.When {
428+
r.write("WHEN ")
429+
r.renderExpr(clause.Condition)
430+
r.write(" THEN ")
431+
r.renderExpr(clause.Result)
432+
r.write(" ")
433+
}
434+
if e.Else != nil {
435+
r.write("ELSE ")
436+
r.renderExpr(e.Else)
437+
r.write(" ")
438+
}
439+
r.write("END")
421440
case *ast.SubqueryExpr:
422441
r.write("(")
423442
r.renderSelect(e.Select)

lib/sql/token/token.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const (
6161
INSERT Type = "INSERT"
6262
UPDATE Type = "UPDATE"
6363
DELETE Type = "DELETE"
64+
CASE Type = "CASE"
6465
INTO Type = "INTO"
6566
VALUES Type = "VALUES"
6667
SET Type = "SET"
@@ -73,6 +74,10 @@ const (
7374
LIMIT Type = "LIMIT"
7475
OFFSET Type = "OFFSET"
7576
AS Type = "AS"
77+
WHEN Type = "WHEN"
78+
THEN Type = "THEN"
79+
ELSE Type = "ELSE"
80+
END Type = "END"
7681
IF Type = "IF"
7782
DISTINCT Type = "DISTINCT"
7883
WITH Type = "WITH"
@@ -124,6 +129,7 @@ var keywords = map[string]Type{
124129
"INSERT": INSERT,
125130
"UPDATE": UPDATE,
126131
"DELETE": DELETE,
132+
"CASE": CASE,
127133
"INTO": INTO,
128134
"VALUES": VALUES,
129135
"SET": SET,
@@ -136,6 +142,10 @@ var keywords = map[string]Type{
136142
"LIMIT": LIMIT,
137143
"OFFSET": OFFSET,
138144
"AS": AS,
145+
"WHEN": WHEN,
146+
"THEN": THEN,
147+
"ELSE": ELSE,
148+
"END": END,
139149
"IF": IF,
140150
"DISTINCT": DISTINCT,
141151
"WITH": WITH,

0 commit comments

Comments
 (0)