mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
Support PQL Range() queries for fields.
This commit is contained in:
parent
4f4843a624
commit
c36fdbfacb
6 changed files with 136 additions and 59 deletions
43
pql/ast.go
43
pql/ast.go
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
pql/token.go
14
pql/token.go
|
|
@ -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: ")",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue