Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spanner/spansql): support case expression #5836

Merged
merged 3 commits into from Apr 5, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 73 additions & 0 deletions spanner/spansql/parser.go
Expand Up @@ -2913,6 +2913,13 @@ func (p *parser) parseLit() (Expr, *parseError) {
// TODO: Check IsKeyWord(tok.value), and return a good error?
}

// Handle conditional expressions.
switch {
case tok.caseEqual("CASE"):
p.back()
return p.parseCaseExpr()
}

// Handle typed literals.
switch {
case tok.caseEqual("ARRAY") || tok.value == "[":
Expand Down Expand Up @@ -2950,6 +2957,72 @@ func (p *parser) parseLit() (Expr, *parseError) {
return pe, nil
}

func (p *parser) parseCaseExpr() (Case, *parseError) {
if err := p.expect("CASE"); err != nil {
return Case{}, err
}

var expr Expr
if !p.sniff("WHEN") {
var err *parseError
expr, err = p.parseExpr()
if err != nil {
return Case{}, err
}
}

when, err := p.parseWhenClause()
if err != nil {
return Case{}, err
}
whens := []WhenClause{when}
for p.sniff("WHEN") {
when, err := p.parseWhenClause()
if err != nil {
return Case{}, err
}
whens = append(whens, when)
}

var elseResult Expr
if p.sniff("ELSE") {
p.eat("ELSE")
var err *parseError
elseResult, err = p.parseExpr()
if err != nil {
return Case{}, err
}
}

if err := p.expect("END"); err != nil {
return Case{}, err
}

return Case{
Expr: expr,
WhenClauses: whens,
ElseResult: elseResult,
}, nil
}

func (p *parser) parseWhenClause() (WhenClause, *parseError) {
if err := p.expect("WHEN"); err != nil {
return WhenClause{}, err
}
cond, err := p.parseExpr()
if err != nil {
return WhenClause{}, err
}
if err := p.expect("THEN"); err != nil {
return WhenClause{}, err
}
result, err := p.parseExpr()
if err != nil {
return WhenClause{}, err
}
return WhenClause{Cond: cond, Result: result}, nil
}

func (p *parser) parseArrayLit() (Array, *parseError) {
// ARRAY keyword is optional.
// TODO: If it is present, consume any <T> after it.
Expand Down
20 changes: 20 additions & 0 deletions spanner/spansql/parser_test.go
Expand Up @@ -343,6 +343,26 @@ func TestParseExpr(t *testing.T) {
{`EXTRACT(DATE FROM TIMESTAMP AT TIME ZONE "America/Los_Angeles")`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DATE", Type: Type{Base: Date}, Expr: AtTimeZoneExpr{Expr: ID("TIMESTAMP"), Zone: "America/Los_Angeles", Type: Type{Base: Timestamp}}}}}},
{`EXTRACT(DAY FROM DATE)`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DAY", Expr: ID("DATE"), Type: Type{Base: Int64}}}}},

// Conditional expressions
{`CASE X WHEN 1 THEN "X" WHEN 2 THEN "Y" ELSE NULL END`,
Case{
Expr: ID("X"),
WhenClauses: []WhenClause{
{Cond: IntegerLiteral(1), Result: StringLiteral("X")},
{Cond: IntegerLiteral(2), Result: StringLiteral("Y")},
},
ElseResult: Null,
},
},
{`CASE WHEN TRUE THEN "X" WHEN FALSE THEN "Y" END`,
Case{
WhenClauses: []WhenClause{
{Cond: True, Result: StringLiteral("X")},
{Cond: False, Result: StringLiteral("Y")},
},
},
},

// String literal:
// Accept double quote and single quote.
{`"hello"`, StringLiteral("hello")},
Expand Down
15 changes: 15 additions & 0 deletions spanner/spansql/sql.go
Expand Up @@ -671,6 +671,21 @@ func (p Param) addSQL(sb *strings.Builder) {
sb.WriteString(string(p))
}

func (c Case) SQL() string { return buildSQL(c) }
func (c Case) addSQL(sb *strings.Builder) {
sb.WriteString("CASE ")
if c.Expr != nil {
fmt.Fprintf(sb, "%s ", c.Expr.SQL())
}
for _, w := range c.WhenClauses {
fmt.Fprintf(sb, "WHEN %s THEN %s ", w.Cond.SQL(), w.Result.SQL())
}
if c.ElseResult != nil {
fmt.Fprintf(sb, "ELSE %s ", c.ElseResult.SQL())
}
sb.WriteString("END")
}

func (b BoolLiteral) SQL() string { return buildSQL(b) }
func (b BoolLiteral) addSQL(sb *strings.Builder) {
if b {
Expand Down
32 changes: 32 additions & 0 deletions spanner/spansql/sql_test.go
Expand Up @@ -566,6 +566,38 @@ func TestSQL(t *testing.T) {
"SELECT A, B FROM Table1 INNER JOIN Table2 ON Table1.A = Table2.A INNER JOIN Table3 USING (X)",
reparseQuery,
},
{
Query{
Select: Select{
List: []Expr{
Case{
Expr: ID("X"),
WhenClauses: []WhenClause{
{Cond: IntegerLiteral(1), Result: StringLiteral("X")},
{Cond: IntegerLiteral(2), Result: StringLiteral("Y")},
},
ElseResult: Null,
}},
},
},
`SELECT CASE X WHEN 1 THEN "X" WHEN 2 THEN "Y" ELSE NULL END`,
reparseQuery,
},
{
Query{
Select: Select{
List: []Expr{
Case{
WhenClauses: []WhenClause{
{Cond: True, Result: StringLiteral("X")},
{Cond: False, Result: StringLiteral("Y")},
},
}},
},
},
`SELECT CASE WHEN TRUE THEN "X" WHEN FALSE THEN "Y" END`,
reparseQuery,
},
}
for _, test := range tests {
sql := test.data.SQL()
Expand Down
14 changes: 14 additions & 0 deletions spanner/spansql/types.go
Expand Up @@ -692,6 +692,20 @@ func (Param) isBoolExpr() {} // possibly bool
func (Param) isExpr() {}
func (Param) isLiteralOrParam() {}

type Case struct {
Expr Expr
WhenClauses []WhenClause
ElseResult Expr
}

func (Case) isBoolExpr() {} // possibly bool
func (Case) isExpr() {}

type WhenClause struct {
Cond Expr
Result Expr
}

type BoolLiteral bool

const (
Expand Down