Bläddra i källkod

feat(parser): support case expression

ngjaying 4 år sedan
förälder
incheckning
5edf71206a
5 ändrade filer med 219 tillägg och 3 borttagningar
  1. 33 0
      xsql/ast.go
  2. 11 2
      xsql/funcs_ast_validator.go
  3. 15 0
      xsql/lexical.go
  4. 67 1
      xsql/parser.go
  5. 93 0
      xsql/parser_test.go

+ 33 - 0
xsql/ast.go

@@ -229,6 +229,28 @@ func (c *Call) expr()    {}
 func (c *Call) literal() {}
 func (c *Call) literal() {}
 func (c *Call) node()    {}
 func (c *Call) node()    {}
 
 
+type WhenClause struct {
+	// The condition expression
+	Expr   Expr
+	Result Expr
+}
+
+func (w *WhenClause) expr()    {}
+func (w *WhenClause) literal() {}
+func (w *WhenClause) node()    {}
+
+type CaseExpr struct {
+	// The compare value expression. It can be a value expression or nil.
+	// When it is nil, the WhenClause Expr must be a logical(comparison) expression
+	Value       Expr
+	WhenClauses []*WhenClause
+	ElseClause  Expr
+}
+
+func (c *CaseExpr) expr()    {}
+func (c *CaseExpr) literal() {}
+func (c *CaseExpr) node()    {}
+
 type WindowType int
 type WindowType int
 
 
 const (
 const (
@@ -440,6 +462,17 @@ func Walk(v Visitor, node Node) {
 	case *Join:
 	case *Join:
 		Walk(v, n.Expr)
 		Walk(v, n.Expr)
 
 
+	case *CaseExpr:
+		Walk(v, n.Value)
+		for _, w := range n.WhenClauses {
+			Walk(v, w)
+		}
+		Walk(v, n.ElseClause)
+
+	case *WhenClause:
+		Walk(v, n.Expr)
+		Walk(v, n.Result)
+
 	case *StreamStmt:
 	case *StreamStmt:
 		Walk(v, &n.Name)
 		Walk(v, &n.Name)
 		Walk(v, n.StreamFields)
 		Walk(v, n.StreamFields)

+ 11 - 2
xsql/funcs_ast_validator.go

@@ -404,10 +404,19 @@ func isFloatArg(arg Expr) bool {
 }
 }
 
 
 func isBooleanArg(arg Expr) bool {
 func isBooleanArg(arg Expr) bool {
-	if _, ok := arg.(*BooleanLiteral); ok {
+	switch t := arg.(type) {
+	case *BooleanLiteral:
 		return true
 		return true
+	case *BinaryExpr:
+		switch t.OP {
+		case AND, OR, EQ, NEQ, LT, LTE, GT, GTE:
+			return true
+		default:
+			return false
+		}
+	default:
+		return false
 	}
 	}
-	return false
 }
 }
 
 
 func isStringArg(arg Expr) bool {
 func isStringArg(arg Expr) bool {

+ 15 - 0
xsql/lexical.go

@@ -83,6 +83,11 @@ const (
 	ASC
 	ASC
 	DESC
 	DESC
 	FILTER
 	FILTER
+	CASE
+	WHEN
+	THEN
+	ELSE
+	END
 
 
 	TRUE
 	TRUE
 	FALSE
 	FALSE
@@ -406,6 +411,16 @@ func (s *Scanner) ScanIdent() (tok Token, lit string) {
 		return JOIN, lit
 		return JOIN, lit
 	case "ON":
 	case "ON":
 		return ON, lit
 		return ON, lit
+	case "CASE":
+		return CASE, lit
+	case "WHEN":
+		return WHEN, lit
+	case "THEN":
+		return THEN, lit
+	case "ELSE":
+		return ELSE, lit
+	case "END":
+		return END, lit
 	case "CREATE":
 	case "CREATE":
 		return CREATE, lit
 		return CREATE, lit
 	case "DROP":
 	case "DROP":

+ 67 - 1
xsql/parser.go

@@ -499,7 +499,9 @@ func (p *Parser) parseUnaryExpr() (Expr, error) {
 	p.unscan()
 	p.unscan()
 
 
 	tok, lit := p.scanIgnoreWhitespace()
 	tok, lit := p.scanIgnoreWhitespace()
-	if tok == IDENT {
+	if tok == CASE {
+		return p.parseCaseExpr()
+	} else if tok == IDENT {
 		if tok1, _ := p.scanIgnoreWhitespace(); tok1 == LPAREN {
 		if tok1, _ := p.scanIgnoreWhitespace(); tok1 == LPAREN {
 			return p.parseCall(lit)
 			return p.parseCall(lit)
 		}
 		}
@@ -670,6 +672,70 @@ func (p *Parser) parseCall(name string) (Expr, error) {
 	}
 	}
 }
 }
 
 
+func (p *Parser) parseCaseExpr() (*CaseExpr, error) {
+	c := &CaseExpr{}
+	tok, _ := p.scanIgnoreWhitespace()
+	p.unscan()
+	if tok != WHEN { // no condition value for case, additional validation needed
+		if exp, err := p.ParseExpr(); err != nil {
+			return nil, err
+		} else {
+			c.Value = exp
+		}
+	}
+
+loop:
+	for {
+		tok, _ := p.scanIgnoreWhitespace()
+		switch tok {
+		case WHEN:
+			if exp, err := p.ParseExpr(); err != nil {
+				return nil, err
+			} else {
+				if c.WhenClauses == nil {
+					c.WhenClauses = make([]*WhenClause, 0)
+				}
+				if c.Value == nil && !isBooleanArg(exp) {
+					return nil, fmt.Errorf("invalid CASE expression, WHEN expression must be a bool condition")
+				}
+				w := &WhenClause{
+					Expr: exp,
+				}
+				tokThen, _ := p.scanIgnoreWhitespace()
+				if tokThen != THEN {
+					return nil, fmt.Errorf("invalid CASE expression, THEN expected after WHEN")
+				} else {
+					if expThen, err := p.ParseExpr(); err != nil {
+						return nil, err
+					} else {
+						w.Result = expThen
+						c.WhenClauses = append(c.WhenClauses, w)
+					}
+				}
+			}
+		case ELSE:
+			if c.WhenClauses != nil {
+				if exp, err := p.ParseExpr(); err != nil {
+					return nil, err
+				} else {
+					c.ElseClause = exp
+				}
+			} else {
+				return nil, fmt.Errorf("invalid CASE expression, WHEN expected before ELSE")
+			}
+		case END:
+			if c.WhenClauses != nil {
+				break loop
+			} else {
+				return nil, fmt.Errorf("invalid CASE expression, WHEN expected before END")
+			}
+		default:
+			return nil, fmt.Errorf("invalid CASE expression, END expected")
+		}
+	}
+	return c, nil
+}
+
 func validateWindows(name string, args []Expr) (WindowType, error) {
 func validateWindows(name string, args []Expr) (WindowType, error) {
 	fname := strings.ToLower(name)
 	fname := strings.ToLower(name)
 	switch fname {
 	switch fname {

+ 93 - 0
xsql/parser_test.go

@@ -1285,6 +1285,99 @@ func TestParser_ParseStatement(t *testing.T) {
 				},
 				},
 				Sources: []Source{&Table{Name: "tbl"}},
 				Sources: []Source{&Table{Name: "tbl"}},
 			},
 			},
+		}, {
+			s: "SELECT CASE temperature WHEN 25 THEN \"bingo\" WHEN 30 THEN \"high\" ELSE \"low\" END as label, humidity FROM tbl",
+			stmt: &SelectStatement{
+				Fields: []Field{
+					{
+						Expr: &CaseExpr{
+							Value: &FieldRef{Name: "temperature"},
+							WhenClauses: []*WhenClause{
+								{
+									Expr:   &IntegerLiteral{Val: 25},
+									Result: &StringLiteral{Val: "bingo"},
+								}, {
+									Expr:   &IntegerLiteral{Val: 30},
+									Result: &StringLiteral{Val: "high"},
+								},
+							},
+							ElseClause: &StringLiteral{Val: "low"},
+						},
+						Name:  "",
+						AName: "label",
+					}, {
+						Expr:  &FieldRef{Name: "humidity"},
+						Name:  "humidity",
+						AName: "",
+					},
+				},
+				Sources: []Source{&Table{Name: "tbl"}},
+			},
+		}, {
+			s: "SELECT CASE temperature WHEN 25 THEN \"bingo\" WHEN 30 THEN \"high\" END as label, humidity FROM tbl",
+			stmt: &SelectStatement{
+				Fields: []Field{
+					{
+						Expr: &CaseExpr{
+							Value: &FieldRef{Name: "temperature"},
+							WhenClauses: []*WhenClause{
+								{
+									Expr:   &IntegerLiteral{Val: 25},
+									Result: &StringLiteral{Val: "bingo"},
+								}, {
+									Expr:   &IntegerLiteral{Val: 30},
+									Result: &StringLiteral{Val: "high"},
+								},
+							},
+							ElseClause: nil,
+						},
+						Name:  "",
+						AName: "label",
+					}, {
+						Expr:  &FieldRef{Name: "humidity"},
+						Name:  "humidity",
+						AName: "",
+					},
+				},
+				Sources: []Source{&Table{Name: "tbl"}},
+			},
+		}, {
+			s:    "SELECT CASE temperature ELSE \"low\" END as label, humidity FROM tbl",
+			stmt: nil,
+			err:  "invalid CASE expression, WHEN expected before ELSE",
+		}, {
+			s: "SELECT CASE WHEN temperature > 30 THEN \"high\" ELSE \"low\" END as label, humidity FROM tbl",
+			stmt: &SelectStatement{
+				Fields: []Field{
+					{
+						Expr: &CaseExpr{
+							Value: nil,
+							WhenClauses: []*WhenClause{
+								{
+									Expr: &BinaryExpr{
+										OP:  GT,
+										LHS: &FieldRef{Name: "temperature"},
+										RHS: &IntegerLiteral{Val: 30},
+									},
+									Result: &StringLiteral{Val: "high"},
+								},
+							},
+							ElseClause: &StringLiteral{Val: "low"},
+						},
+						Name:  "",
+						AName: "label",
+					}, {
+						Expr:  &FieldRef{Name: "humidity"},
+						Name:  "humidity",
+						AName: "",
+					},
+				},
+				Sources: []Source{&Table{Name: "tbl"}},
+			},
+		}, {
+			s:    "SELECT CASE WHEN 30 THEN \"high\" ELSE \"low\" END as label, humidity FROM tbl",
+			stmt: nil,
+			err:  "invalid CASE expression, WHEN expression must be a bool condition",
 		},
 		},
 	}
 	}