Add first part of query parser / Finish query building

This commit is contained in:
Daniel 2018-08-30 19:03:45 +02:00
parent 6ed50f34fb
commit 115b18dfb6
17 changed files with 337 additions and 66 deletions

View file

@ -1,5 +1,10 @@
package query package query
import (
"fmt"
"strings"
)
// And combines multiple conditions with a logical _AND_ operator. // And combines multiple conditions with a logical _AND_ operator.
func And(conditions ...Condition) Condition { func And(conditions ...Condition) Condition {
return &andCond{ return &andCond{
@ -29,3 +34,11 @@ func (c *andCond) check() (err error) {
} }
return nil return nil
} }
func (c *andCond) string() string {
var all []string
for _, cond := range c.conditions {
all = append(all, cond.string())
}
return fmt.Sprintf("(%s)", strings.Join(all, " and "))
}

View file

@ -62,3 +62,7 @@ func (c *boolCondition) check() error {
} }
return nil return nil
} }
func (c *boolCondition) string() string {
return fmt.Sprintf("%s %s %t", c.key, getOpName(c.operator), c.value)
}

View file

@ -17,3 +17,7 @@ func (c *errorCondition) complies(f Fetcher) bool {
func (c *errorCondition) check() error { func (c *errorCondition) check() error {
return c.err return c.err
} }
func (c *errorCondition) string() string {
return "[ERROR]"
}

View file

@ -2,6 +2,7 @@ package query
import ( import (
"errors" "errors"
"fmt"
) )
type existsCondition struct { type existsCondition struct {
@ -26,3 +27,7 @@ func (c *existsCondition) check() error {
} }
return nil return nil
} }
func (c *existsCondition) string() string {
return fmt.Sprintf("%s %s", c.key, getOpName(c.operator))
}

View file

@ -88,3 +88,7 @@ func (c *floatCondition) check() error {
} }
return nil return nil
} }
func (c *floatCondition) string() string {
return fmt.Sprintf("%s %s %f", c.key, getOpName(c.operator), c.value)
}

View file

@ -84,3 +84,7 @@ func (c *intCondition) check() error {
} }
return nil return nil
} }
func (c *intCondition) string() string {
return fmt.Sprintf("%s %s %d", c.key, getOpName(c.operator), c.value)
}

View file

@ -0,0 +1,16 @@
package query
type noCond struct {
}
func (c *noCond) complies(f Fetcher) bool {
return true
}
func (c *noCond) check() (err error) {
return nil
}
func (c *noCond) string() string {
return ""
}

View file

@ -1,5 +1,10 @@
package query package query
import (
"fmt"
"strings"
)
// Not negates the supplied condition. // Not negates the supplied condition.
func Not(c Condition) Condition { func Not(c Condition) Condition {
return &notCond{ return &notCond{
@ -18,3 +23,12 @@ func (c *notCond) complies(f Fetcher) bool {
func (c *notCond) check() error { func (c *notCond) check() error {
return c.notC.check() return c.notC.check()
} }
func (c *notCond) string() string {
next := c.notC.string()
if strings.HasPrefix(next, "(") {
return fmt.Sprintf("not %s", c.notC.string())
}
splitted := strings.Split(next, " ")
return strings.Join(append([]string{splitted[0], "not"}, splitted[1:]...), " ")
}

View file

@ -1,5 +1,10 @@
package query package query
import (
"fmt"
"strings"
)
// Or combines multiple conditions with a logical _OR_ operator. // Or combines multiple conditions with a logical _OR_ operator.
func Or(conditions ...Condition) Condition { func Or(conditions ...Condition) Condition {
return &orCond{ return &orCond{
@ -29,3 +34,11 @@ func (c *orCond) check() (err error) {
} }
return nil return nil
} }
func (c *orCond) string() string {
var all []string
for _, cond := range c.conditions {
all = append(all, cond.string())
}
return fmt.Sprintf("(%s)", strings.Join(all, " or "))
}

View file

@ -55,3 +55,7 @@ func (c *regexCondition) check() error {
} }
return nil return nil
} }
func (c *regexCondition) string() string {
return fmt.Sprintf("%s %s %s", c.key, getOpName(c.operator), c.regex.String())
}

View file

@ -35,7 +35,7 @@ func (c *stringCondition) complies(f Fetcher) bool {
} }
switch c.operator { switch c.operator {
case Matches: case SameAs:
return c.value == comp return c.value == comp
case Contains: case Contains:
return strings.Contains(comp, c.value) return strings.Contains(comp, c.value)
@ -54,3 +54,7 @@ func (c *stringCondition) check() error {
} }
return nil return nil
} }
func (c *stringCondition) string() string {
return fmt.Sprintf("%s %s %s", c.key, getOpName(c.operator), c.value)
}

View file

@ -58,3 +58,7 @@ func (c *stringSliceCondition) check() error {
} }
return nil return nil
} }
func (c *stringSliceCondition) string() string {
return fmt.Sprintf("%s %s %s", c.key, getOpName(c.operator), strings.Join(c.value, ","))
}

View file

@ -6,7 +6,7 @@ import "fmt"
type Condition interface { type Condition interface {
complies(f Fetcher) bool complies(f Fetcher) bool
check() error check() error
// string() string string() string
} }
// Operators // Operators

View file

@ -1,5 +1,12 @@
package query package query
import (
"errors"
"fmt"
"regexp"
"strings"
)
var ( var (
operatorNames = map[string]uint8{ operatorNames = map[string]uint8{
"==": Equals, "==": Equals,
@ -27,18 +34,194 @@ var (
"exists": Exists, "exists": Exists,
"ex": Exists, "ex": Exists,
} }
primaryNames = make(map[uint8]string)
) )
func getOpName(operator uint8) string { func init() {
for opName, op := range operatorNames { for opName, opID := range operatorNames {
if op == operator { name, ok := primaryNames[opID]
return opName if ok {
if len(name) < len(opName) {
primaryNames[opID] = opName
}
} else {
primaryNames[opID] = opName
} }
} }
}
func getOpName(operator uint8) string {
name, ok := primaryNames[operator]
if ok {
return name
}
return "[unknown]" return "[unknown]"
} }
// ParseQuery parses a plaintext query. type treeElement struct {
branches []*treeElement
text string
globalPosition int
}
var (
escapeReplacer = regexp.MustCompile("\\\\([^\\\\])")
)
// prepToken removes surrounding parenthesis and escape characters.
func prepToken(text string) string {
return escapeReplacer.ReplaceAllString(strings.Trim(text, "\""), "$1")
}
func extractSnippets(text string) (snippets []*treeElement, err error) {
skip := false
start := -1
inParenthesis := false
var pos int
var char rune
for pos, char = range text {
// skip
if skip {
skip = false
continue
}
if char == '\\' {
skip = true
}
// wait for parenthesis to be over
if inParenthesis {
if char == '"' {
snippets = append(snippets, &treeElement{
text: prepToken(text[start+1 : pos]),
globalPosition: start + 1,
})
start = -1
inParenthesis = false
}
continue
}
// handle segments
switch char {
case '\t', '\n', '\r', ' ', '(', ')':
if start >= 0 {
snippets = append(snippets, &treeElement{
text: prepToken(text[start:pos]),
globalPosition: start,
})
start = -1
}
default:
if start == -1 {
start = pos
}
}
// handle special segment characters
switch char {
case '(', ')':
snippets = append(snippets, &treeElement{
text: text[pos : pos+1],
globalPosition: pos,
})
case '"':
if start < pos {
return nil, fmt.Errorf("parenthesis ('\"') may not be within words, please escape with '\\' (position: %d)", pos+1)
}
inParenthesis = true
}
}
// add last
snippets = append(snippets, &treeElement{
text: prepToken(text[start : pos+1]),
globalPosition: start,
})
return snippets, nil
}
// ParseQuery parses a plaintext query. Special characters (that must be escaped with a '\') are: `\()` and any whitespaces.
func ParseQuery(query string) (*Query, error) { func ParseQuery(query string) (*Query, error) {
snippets, err := extractSnippets(query)
if err != nil {
return nil, err
}
snippetsPos := 0
getElement := func() (*treeElement, error) {
if snippetsPos >= len(snippets) {
return nil, fmt.Errorf("unexpected end at position %d", len(query))
}
return snippets[snippetsPos], nil
}
// check for query word
queryWord, err := getElement()
if err != nil {
return nil, err
}
if queryWord.text != "query" {
return nil, errors.New("queries must start with \"query\"")
}
// get prefix
prefix, err := getElement()
if err != nil {
return nil, err
}
// check if no condition
if len(snippets) == 2 {
return New(prefix.text, nil)
}
// check for where word
whereWord, err := getElement()
if err != nil {
return nil, err
}
if whereWord.text != "where" {
return nil, errors.New("filtering queries must start conditions with \"where\"")
}
// parse conditions
condition, err := parseCondition(getElement)
if err != nil {
return nil, err
}
// check for additional tokens
// token := s.Scan()
// if token != scanner.EOF {
// return nil, fmt.Errorf("unexpected additional tokens at position %d", s.Position)
// }
return New(prefix.text, condition)
}
func parseCondition(getElement func() (*treeElement, error)) (Condition, error) {
first, err := getElement()
if err != nil {
return nil, err
}
switch first.text {
case "(":
return parseAndOr(getElement, true, nil, false)
// case ""
}
return nil, nil
}
func parseAndOr(getElement func() (*treeElement, error), expectBracket bool, preParsedCondition Condition, preParsedIsOr bool) (Condition, error) {
return nil, nil return nil, nil
} }

View file

@ -0,0 +1,18 @@
package query
import (
"testing"
)
func TestMakeTree(t *testing.T) {
text1 := `(age > 100 and experience <= "99") or (age < 10 and motivation > 50) or name matches Mr.\"X or name matches y`
snippets, err := extractSnippets(text1)
if err != nil {
t.Errorf("failed to make tree: %s", err)
} else {
for _, el := range snippets {
t.Errorf("%+v", el)
}
}
// t.Error(spew.Sprintf("%v", treeElement))
}

View file

@ -3,6 +3,7 @@ package query
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strings"
) )
// Example: // Example:
@ -21,78 +22,56 @@ var (
// Query contains a compiled query. // Query contains a compiled query.
type Query struct { type Query struct {
prefix string prefix string
conditions []Condition condition Condition
} }
// New creates a new query. // New creates a new query.
func New(prefix string, conditions ...Condition) (*Query, error) { func New(prefix string, condition Condition) (*Query, error) {
// check prefix // check prefix
if !prefixExpr.MatchString(prefix) { if !prefixExpr.MatchString(prefix) {
return nil, fmt.Errorf("invalid prefix: %s", prefix) return nil, fmt.Errorf("invalid prefix: %s", prefix)
} }
// check conditions // check condition
var err error if condition != nil {
for _, cond := range conditions { err := condition.check()
err = cond.check()
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
condition = &noCond{}
} }
// return query // return query
return &Query{ return &Query{
prefix: prefix, prefix: prefix,
conditions: conditions, condition: condition,
}, nil }, nil
} }
// MustCompile creates a new query and panics on an error. // MustCompile creates a new query and panics on an error.
func MustCompile(prefix string, conditions ...Condition) *Query { func MustCompile(prefix string, condition Condition) *Query {
q, err := New(prefix, conditions...) q, err := New(prefix, condition)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return q return q
} }
// Prepend prepends (check first) new query conditions to the query.
func (q *Query) Prepend(conditions ...Condition) error {
// check conditions
var err error
for _, cond := range conditions {
err = cond.check()
if err != nil {
return err
}
}
q.conditions = append(conditions, q.conditions...)
return nil
}
// Append appends (check last) new query conditions to the query.
func (q *Query) Append(conditions ...Condition) error {
// check conditions
var err error
for _, cond := range conditions {
err = cond.check()
if err != nil {
return err
}
}
q.conditions = append(q.conditions, conditions...)
return nil
}
// Matches checks whether the query matches the supplied data object. // Matches checks whether the query matches the supplied data object.
func (q *Query) Matches(f Fetcher) bool { func (q *Query) Matches(f Fetcher) bool {
for _, cond := range q.conditions { return q.condition.complies(f)
if !cond.complies(f) { }
return false
} // String returns the string representation of the query.
} func (q *Query) String() string {
return true text := q.condition.string()
if text == "" {
return fmt.Sprintf("query %s", q.prefix)
}
if strings.HasPrefix(text, "(") {
text = text[1 : len(text)-1]
}
return fmt.Sprintf("query %s where %s", q.prefix, text)
} }

View file

@ -45,18 +45,16 @@ var (
) )
func testQuery(t *testing.T, f Fetcher, shouldMatch bool, condition Condition) { func testQuery(t *testing.T, f Fetcher, shouldMatch bool, condition Condition) {
q := MustCompile("test:") q := MustCompile("test:", condition)
err := q.Append(condition)
if err != nil { // fmt.Printf("%s\n", q.String())
t.Errorf("append failed: %s", err)
}
matched := q.Matches(f) matched := q.Matches(f)
switch { switch {
case matched && !shouldMatch:
t.Errorf("query should match")
case !matched && shouldMatch: case !matched && shouldMatch:
t.Errorf("query should not match") t.Errorf("should match: %s", q.String())
case matched && !shouldMatch:
t.Errorf("should not match: %s", q.String())
} }
} }
@ -83,7 +81,7 @@ func TestQuery(t *testing.T) {
testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "121")) testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "121"))
testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "120.413")) testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "120.413"))
testQuery(t, f, true, Where("lastly.yay", Matches, "final")) testQuery(t, f, true, Where("lastly.yay", SameAs, "final"))
testQuery(t, f, true, Where("lastly.yay", Contains, "ina")) testQuery(t, f, true, Where("lastly.yay", Contains, "ina"))
testQuery(t, f, true, Where("lastly.yay", StartsWith, "fin")) testQuery(t, f, true, Where("lastly.yay", StartsWith, "fin"))
testQuery(t, f, true, Where("lastly.yay", EndsWith, "nal")) testQuery(t, f, true, Where("lastly.yay", EndsWith, "nal"))
@ -92,10 +90,14 @@ func TestQuery(t *testing.T) {
testQuery(t, f, true, Where("happy", Is, "true")) testQuery(t, f, true, Where("happy", Is, "true"))
testQuery(t, f, true, Where("happy", Is, "t")) testQuery(t, f, true, Where("happy", Is, "t"))
testQuery(t, f, true, Where("happy", Is, "1"))
testQuery(t, f, true, Not(Where("happy", Is, "false")))
testQuery(t, f, true, Not(Where("happy", Is, "f")))
testQuery(t, f, true, Not(Where("happy", Is, "0"))) testQuery(t, f, true, Not(Where("happy", Is, "0")))
testQuery(t, f, true, And(
Where("happy", Is, "1"),
Not(Or(
Where("happy", Is, false),
Where("happy", Is, "f"),
)),
))
testQuery(t, f, true, Where("happy", Exists, nil)) testQuery(t, f, true, Where("happy", Exists, nil))