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.
This commit is contained in:
Tobie Morgan Hitchcock 2017-04-14 13:21:08 +01:00
parent 2de5a8fa3f
commit aa7e4ac561
4 changed files with 176 additions and 1 deletions

View file

@ -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
// --------------------------------------------------

View file

@ -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.

View file

@ -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 == 'µ'

View file

@ -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",