Support PQL Range() queries for fields.

This commit is contained in:
Ben Johnson 2017-07-20 11:20:58 -06:00
parent 4f4843a624
commit c36fdbfacb
No known key found for this signature in database
GPG key ID: 81741CD251883081
6 changed files with 136 additions and 59 deletions

View file

@ -161,19 +161,7 @@ func (c *Call) String() string {
if i > 0 {
buf.WriteString(", ")
}
switch v := c.Args[key].(type) {
case string:
fmt.Fprintf(&buf, "%v=%q", key, v)
case []interface{}:
fmt.Fprintf(&buf, "%v=%s", key, joinInterfaceSlice(v))
case []uint64:
fmt.Fprintf(&buf, "%v=%s", key, joinUint64Slice(v))
case time.Time:
fmt.Fprintf(&buf, "%v=\"%s\"", key, v.Format(TimeFormat))
default:
fmt.Fprintf(&buf, "%v=%v", key, v)
}
fmt.Fprintf(&buf, "%v=%s", key, FormatValue(c.Args[key]))
}
// Write closing.
@ -210,6 +198,35 @@ func (c *Call) IsInverse(rowLabel, columnLabel string) bool {
return false
}
// Condition represents an operation & value.
// When used in an argument map it represents a binary expression.
type Condition struct {
Op Token
Value interface{}
}
// String returns the string representation of the condition.
func (cond *Condition) String() string {
return fmt.Sprintf("%s %s", cond.Op.String(), FormatValue(cond.Value))
}
func FormatValue(v interface{}) string {
switch v := v.(type) {
case string:
return fmt.Sprintf("%q", v)
case []interface{}:
return fmt.Sprintf("%s", joinInterfaceSlice(v))
case []uint64:
return fmt.Sprintf("%s", joinUint64Slice(v))
case time.Time:
return fmt.Sprintf("\"%s\"", v.Format(TimeFormat))
case *Condition:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
// CopyArgs returns a copy of m.
func CopyArgs(m map[string]interface{}) map[string]interface{} {
other := make(map[string]interface{}, len(m))

View file

@ -161,9 +161,14 @@ func (p *Parser) parseArgs() (map[string]interface{}, error) {
}
key := lit
// Expect '=' next.
if tok, pos, lit := p.scanIgnoreWhitespace(); tok != EQ {
return nil, parseErrorf(pos, "expected equals sign, found %q", lit)
// Expect '=' or a comparison next.
var op Token
switch tok, pos, lit := p.scanIgnoreWhitespace(); tok {
case ASSIGN:
case EQ, LT, LTE, GT, GTE:
op = tok
default:
return nil, parseErrorf(pos, "expected equals sign or comparison operator, found %q", lit)
}
// Parse value.
@ -209,6 +214,11 @@ func (p *Parser) parseArgs() (map[string]interface{}, error) {
return nil, parseErrorf(pos, "argument key already used: %s", key)
}
// If op is specified then create a condition.
if op != 0 {
value = &Condition{Op: op, Value: value}
}
// Add key/value pair to arguments.
args[key] = value

View file

@ -169,4 +169,24 @@ func TestParser_Parse(t *testing.T) {
t.Fatalf("unexpected call: %#v", q.Calls[0])
}
})
// Parse with condition arguments.
t.Run("WithCondition", func(t *testing.T) {
q, err := pql.ParseString(`MyCall(key=foo, x == 12.25, y >= 100)`)
if err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(q.Calls[0],
&pql.Call{
Name: "MyCall",
Args: map[string]interface{}{
"key": "foo",
"x": &pql.Condition{Op: pql.EQ, Value: 12.25},
"y": &pql.Condition{Op: pql.GTE, Value: int64(100)},
},
},
) {
t.Fatalf("unexpected call: %#v", q.Calls[0])
}
})
}

View file

@ -59,26 +59,38 @@ func (s *Scanner) Scan() (tok Token, pos Pos, lit string) {
// Otherwise parse individual characters.
switch ch {
case eof:
tok = EOF
return
return EOF, pos, ""
case '=':
tok = EQ
if next := s.read(); next == '=' {
return EQ, pos, "=="
}
s.unread()
return ASSIGN, pos, string(ch)
case '<':
if next := s.read(); next == '=' {
return LTE, pos, "<="
}
s.unread()
return LT, pos, string(ch)
case '>':
if next := s.read(); next == '=' {
return GTE, pos, ">="
}
s.unread()
return GT, pos, string(ch)
case ',':
tok = COMMA
return COMMA, pos, string(ch)
case '(':
tok = LPAREN
return LPAREN, pos, string(ch)
case ')':
tok = RPAREN
return RPAREN, pos, string(ch)
case '[':
tok = LBRACK
return LBRACK, pos, string(ch)
case ']':
tok = RBRACK
return RBRACK, pos, string(ch)
default:
tok = ILLEGAL
return ILLEGAL, pos, string(ch)
}
lit = string(ch)
return
}
// read returns the next code point from the underlying reader and updates the pos.

View file

@ -23,42 +23,50 @@ import (
func TestScanner_Scan(t *testing.T) {
var tests = []struct {
s string
tok pql.Token
lit string
pos pql.Pos
name string
s string
tok pql.Token
lit string
pos pql.Pos
}{
// Special tokens (EOF, ILLEGAL, WS)
{s: ``, tok: pql.EOF},
{s: `#`, tok: pql.ILLEGAL, lit: `#`},
{s: ` `, tok: pql.WS, lit: " "},
{s: "\t", tok: pql.WS, lit: "\t"},
{s: "\n", tok: pql.WS, lit: "\n"},
{name: "EOF", s: ``, tok: pql.EOF},
{name: "ILLEGAL", s: `#`, tok: pql.ILLEGAL, lit: `#`},
{name: "WS/SPACE", s: ` `, tok: pql.WS, lit: " "},
{name: "WS/TAB", s: "\t", tok: pql.WS, lit: "\t"},
{name: "WS/NEWLINE", s: "\n", tok: pql.WS, lit: "\n"},
{s: `=`, tok: pql.EQ, lit: `=`},
{s: `,`, tok: pql.COMMA, lit: `,`},
{s: `(`, tok: pql.LPAREN, lit: `(`},
{s: `)`, tok: pql.RPAREN, lit: `)`},
{s: `[`, tok: pql.LBRACK, lit: `[`},
{s: `]`, tok: pql.RBRACK, lit: `]`},
{name: "ASSIGN", s: `=`, tok: pql.ASSIGN, lit: `=`},
{name: "EQ", s: `==`, tok: pql.EQ, lit: `==`},
{name: "LT", s: `<`, tok: pql.LT, lit: `<`},
{name: "LTE", s: `<=`, tok: pql.LTE, lit: `<=`},
{name: "GT", s: `>`, tok: pql.GT, lit: `>`},
{name: "GTE", s: `>=`, tok: pql.GTE, lit: `>=`},
{name: "COMMA", s: `,`, tok: pql.COMMA, lit: `,`},
{name: "LPAREN", s: `(`, tok: pql.LPAREN, lit: `(`},
{name: "RPAREN", s: `)`, tok: pql.RPAREN, lit: `)`},
{name: "LBRACK", s: `[`, tok: pql.LBRACK, lit: `[`},
{name: "RBRACK", s: `]`, tok: pql.RBRACK, lit: `]`},
{s: `foo`, tok: pql.IDENT, lit: `foo`},
{s: `100`, tok: pql.INTEGER, lit: `100`},
{s: `100.3`, tok: pql.FLOAT, lit: `100.3`},
{name: "IDENT", s: `foo`, tok: pql.IDENT, lit: `foo`},
{name: "INTEGER", s: `100`, tok: pql.INTEGER, lit: `100`},
{name: "FLOAT", s: `100.3`, tok: pql.FLOAT, lit: `100.3`},
{s: `all`, tok: pql.ALL, lit: `all`},
{s: `ALL`, tok: pql.ALL, lit: `ALL`}, // case insensitive
{name: "ALL", s: `all`, tok: pql.ALL, lit: `all`},
{name: "ALL/CASE", s: `ALL`, tok: pql.ALL, lit: `ALL`}, // case insensitive
}
for i, tt := range tests {
s := pql.NewScanner(strings.NewReader(tt.s))
tok, pos, lit := s.Scan()
if tt.tok != tok {
t.Errorf("%d. %q token mismatch: exp=%q got=%q <%q>", i, tt.s, tt.tok, tok, lit)
} else if tt.pos.Line != pos.Line || tt.pos.Char != pos.Char {
t.Errorf("%d. %q pos mismatch: exp=%#v got=%#v", i, tt.s, tt.pos, pos)
} else if tt.lit != lit {
t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, tt.s, tt.lit, lit)
}
t.Run(tt.name, func(t *testing.T) {
s := pql.NewScanner(strings.NewReader(tt.s))
tok, pos, lit := s.Scan()
if tt.tok != tok {
t.Errorf("%d. %q token mismatch: exp=%q got=%q <%q>", i, tt.s, tt.tok, tok, lit)
} else if tt.pos.Line != pos.Line || tt.pos.Char != pos.Char {
t.Errorf("%d. %q pos mismatch: exp=%#v got=%#v", i, tt.s, tt.pos, pos)
} else if tt.lit != lit {
t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, tt.s, tt.lit, lit)
}
})
}
}

View file

@ -37,7 +37,12 @@ const (
ALL
keyword_end
EQ // =
ASSIGN // =
EQ // ==
LT // <
LTE // <=
GT // >
GTE // >=
COMMA // ,
LPAREN // (
RPAREN // )
@ -56,7 +61,12 @@ var tokens = [...]string{
ALL: "ALL",
EQ: "=",
ASSIGN: "=",
EQ: "==",
LT: "<",
LTE: "<=",
GT: ">",
GTE: ">=",
COMMA: ",",
LPAREN: "(",
RPAREN: ")",