From aa7e4ac561fbdc245afc822b36bd71f7c142bb95 Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Fri, 14 Apr 2017 13:21:08 +0100 Subject: [PATCH] Enable model expressions in SQL parser The SQL parser now supports model expressions for defining ranges using the |table:0,0.5..100| notation syntax. --- sql/ast.go | 16 ++++++ sql/exprs.go | 7 ++- sql/scanner.go | 152 +++++++++++++++++++++++++++++++++++++++++++++++++ sql/tokens.go | 2 + 4 files changed, 176 insertions(+), 1 deletion(-) diff --git a/sql/ast.go b/sql/ast.go index 760ff7ae..3e0814d3 100644 --- a/sql/ast.go +++ b/sql/ast.go @@ -551,6 +551,22 @@ type ContentExpression struct { Data Expr } +// -------------------------------------------------- +// Model +// -------------------------------------------------- + +// Model comment +type Model struct { + TB string + MIN float64 + INC float64 + MAX float64 +} + +func NewModel(TB string, MIN, INC, MAX float64) *Model { + return &Model{TB: TB, MIN: MIN, INC: INC, MAX: MAX} +} + // -------------------------------------------------- // Param // -------------------------------------------------- diff --git a/sql/exprs.go b/sql/exprs.go index f1f32cda..27eb9f26 100644 --- a/sql/exprs.go +++ b/sql/exprs.go @@ -28,7 +28,7 @@ func (p *parser) parseWhat() (mul []Expr, err error) { for { - tok, lit, err := p.shouldBe(IDENT, THING, PARAM) + tok, lit, err := p.shouldBe(IDENT, THING, PARAM, MODEL) if err != nil { return nil, &ParseError{Found: lit, Expected: []string{"table, or thing"}} } @@ -48,6 +48,11 @@ func (p *parser) parseWhat() (mul []Expr, err error) { mul = append(mul, one) } + if p.is(tok, MODEL) { + one, _ := p.declare(MODEL, lit) + mul = append(mul, one) + } + // Check to see if the next token is a comma // and if not, then break out of the loop, // otherwise repeat until we find no comma. diff --git a/sql/scanner.go b/sql/scanner.go index b5dcd77f..9f103854 100644 --- a/sql/scanner.go +++ b/sql/scanner.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "regexp" + "strconv" "strings" "time" ) @@ -136,6 +137,7 @@ func (s *scanner) scan() (tok Token, lit string, val interface{}) { return OR, "OR", val default: s.undo() + return s.scanModel(ch) } case '&': chn := s.next() @@ -461,6 +463,151 @@ func (s *scanner) scanThing(chp ...rune) (tok Token, lit string, val interface{} } +func (s *scanner) scanModel(chp ...rune) (tok Token, lit string, val interface{}) { + + var com bool + var dot bool + + var tbv string + var min float64 = 0 + var inc float64 = 0 + var max float64 = 0 + + // Create a buffer + var buf bytes.Buffer + + // Read passed in runes + for _, ch := range chp { + buf.WriteRune(ch) + } + + if tok, tbv, _ = s.part(); tok == ILLEGAL { + buf.WriteString(tbv) + return ILLEGAL, buf.String(), val + } else { + buf.WriteString(tbv) + } + + if ch := s.next(); ch == ':' { + buf.WriteRune(ch) + } else { + return ILLEGAL, buf.String(), val + } + + if ch := s.next(); isSignal(ch) { + tok, lit, _ = s.scanSignal(ch) + buf.WriteString(lit) + max, _ = strconv.ParseFloat(lit, 64) + } else { + return ILLEGAL, buf.String(), val + } + + if ch := s.next(); ch == ',' { + com = true + buf.WriteRune(ch) + if ch := s.next(); isSignal(ch) { + tok, lit, _ = s.scanSignal(ch) + buf.WriteString(lit) + inc, _ = strconv.ParseFloat(lit, 64) + } else { + return ILLEGAL, buf.String(), val + } + } else { + s.undo() + } + + if ch := s.next(); ch == '.' { + dot = true + buf.WriteRune(ch) + if ch := s.next(); ch == '.' { + buf.WriteRune(ch) + if ch := s.next(); isSignal(ch) { + tok, lit, _ = s.scanSignal(ch) + buf.WriteString(lit) + min = max + max, _ = strconv.ParseFloat(lit, 64) + } else { + return ILLEGAL, buf.String(), val + } + } else { + return ILLEGAL, buf.String(), val + } + } else { + s.undo() + } + + if ch := s.next(); ch == '|' { + buf.WriteRune(ch) + } else { + return ILLEGAL, buf.String(), val + } + + // If we have a comma, but the + // value is below zero, we will + // error as this will cause an + // infinite loop in db. + + if com == true && inc <= 0 { + return ILLEGAL, buf.String(), val + } + + // If we have a min, and a max + // with .. notation, but no `inc` + // is specified, set the `inc` to + // a default of `1`. + + if dot == true && inc <= 0 { + inc = 1 + } + + // If we have a comma, but no + // max value is specified then + // error, as we need a max with + // incrementing integer ids. + + if com == true && dot == false { + return ILLEGAL, buf.String(), val + } + + return MODEL, buf.String(), NewModel(tbv, min, inc, max) + +} + +func (s *scanner) scanSignal(chp ...rune) (tok Token, lit string, val interface{}) { + + // Create a buffer + var buf bytes.Buffer + + // Read passed in runes + for _, ch := range chp { + buf.WriteRune(ch) + } + + // Read subsequent characters + for { + if ch := s.next(); ch == eof { + break + } else if isNumber(ch) { + buf.WriteRune(ch) + } else if ch == '.' { + if s.next() == '.' { + s.undo() + s.undo() + break + } else { + s.undo() + buf.WriteRune(ch) + } + } else { + s.undo() + break + } + } + + return NUMBER, buf.String(), nil + +} + func (s *scanner) scanNumber(chp ...rune) (tok Token, lit string, val interface{}) { tok = NUMBER @@ -783,6 +930,11 @@ func isNumber(ch rune) bool { return (ch >= '0' && ch <= '9') } +// isSignal returns true if the rune is a number. +func isSignal(ch rune) bool { + return (ch >= '0' && ch <= '9') || ch == '-' || ch == '+' +} + // isLetter returns true if the rune is a letter. func isLetter(ch rune) bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == 'ยต' diff --git a/sql/tokens.go b/sql/tokens.go index 86261ee6..372300f2 100644 --- a/sql/tokens.go +++ b/sql/tokens.go @@ -37,6 +37,7 @@ const ( EXPR // something[0].value IDENT // something THING // @class:id + MODEL // [person|1..1000] STRING // "something" REGION // "a multiline \n string" NUMBER // 123456 @@ -214,6 +215,7 @@ var tokens = [...]string{ EXPR: "EXPR", IDENT: "IDENT", THING: "THING", + MODEL: "MODEL", STRING: "STRING", REGION: "REGION", NUMBER: "NUMBER",