mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
Add first part of query parser / Finish query building
This commit is contained in:
parent
6ed50f34fb
commit
115b18dfb6
17 changed files with 337 additions and 66 deletions
|
@ -1,5 +1,10 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// And combines multiple conditions with a logical _AND_ operator.
|
||||
func And(conditions ...Condition) Condition {
|
||||
return &andCond{
|
||||
|
@ -29,3 +34,11 @@ func (c *andCond) check() (err error) {
|
|||
}
|
||||
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 "))
|
||||
}
|
||||
|
|
|
@ -62,3 +62,7 @@ func (c *boolCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *boolCondition) string() string {
|
||||
return fmt.Sprintf("%s %s %t", c.key, getOpName(c.operator), c.value)
|
||||
}
|
||||
|
|
|
@ -17,3 +17,7 @@ func (c *errorCondition) complies(f Fetcher) bool {
|
|||
func (c *errorCondition) check() error {
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *errorCondition) string() string {
|
||||
return "[ERROR]"
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package query
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type existsCondition struct {
|
||||
|
@ -26,3 +27,7 @@ func (c *existsCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *existsCondition) string() string {
|
||||
return fmt.Sprintf("%s %s", c.key, getOpName(c.operator))
|
||||
}
|
||||
|
|
|
@ -88,3 +88,7 @@ func (c *floatCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *floatCondition) string() string {
|
||||
return fmt.Sprintf("%s %s %f", c.key, getOpName(c.operator), c.value)
|
||||
}
|
||||
|
|
|
@ -84,3 +84,7 @@ func (c *intCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *intCondition) string() string {
|
||||
return fmt.Sprintf("%s %s %d", c.key, getOpName(c.operator), c.value)
|
||||
}
|
||||
|
|
16
database/query/condition-no.go
Normal file
16
database/query/condition-no.go
Normal 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 ""
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Not negates the supplied condition.
|
||||
func Not(c Condition) Condition {
|
||||
return ¬Cond{
|
||||
|
@ -18,3 +23,12 @@ func (c *notCond) complies(f Fetcher) bool {
|
|||
func (c *notCond) check() error {
|
||||
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:]...), " ")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Or combines multiple conditions with a logical _OR_ operator.
|
||||
func Or(conditions ...Condition) Condition {
|
||||
return &orCond{
|
||||
|
@ -29,3 +34,11 @@ func (c *orCond) check() (err error) {
|
|||
}
|
||||
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 "))
|
||||
}
|
||||
|
|
|
@ -55,3 +55,7 @@ func (c *regexCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *regexCondition) string() string {
|
||||
return fmt.Sprintf("%s %s %s", c.key, getOpName(c.operator), c.regex.String())
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func (c *stringCondition) complies(f Fetcher) bool {
|
|||
}
|
||||
|
||||
switch c.operator {
|
||||
case Matches:
|
||||
case SameAs:
|
||||
return c.value == comp
|
||||
case Contains:
|
||||
return strings.Contains(comp, c.value)
|
||||
|
@ -54,3 +54,7 @@ func (c *stringCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stringCondition) string() string {
|
||||
return fmt.Sprintf("%s %s %s", c.key, getOpName(c.operator), c.value)
|
||||
}
|
||||
|
|
|
@ -58,3 +58,7 @@ func (c *stringSliceCondition) check() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stringSliceCondition) string() string {
|
||||
return fmt.Sprintf("%s %s %s", c.key, getOpName(c.operator), strings.Join(c.value, ","))
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import "fmt"
|
|||
type Condition interface {
|
||||
complies(f Fetcher) bool
|
||||
check() error
|
||||
// string() string
|
||||
string() string
|
||||
}
|
||||
|
||||
// Operators
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
operatorNames = map[string]uint8{
|
||||
"==": Equals,
|
||||
|
@ -27,18 +34,194 @@ var (
|
|||
"exists": Exists,
|
||||
"ex": Exists,
|
||||
}
|
||||
|
||||
primaryNames = make(map[uint8]string)
|
||||
)
|
||||
|
||||
func getOpName(operator uint8) string {
|
||||
for opName, op := range operatorNames {
|
||||
if op == operator {
|
||||
return opName
|
||||
func init() {
|
||||
for opName, opID := range operatorNames {
|
||||
name, ok := primaryNames[opID]
|
||||
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]"
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
|
18
database/query/parser_test.go
Normal file
18
database/query/parser_test.go
Normal 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))
|
||||
}
|
|
@ -3,6 +3,7 @@ package query
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Example:
|
||||
|
@ -21,78 +22,56 @@ var (
|
|||
|
||||
// Query contains a compiled query.
|
||||
type Query struct {
|
||||
prefix string
|
||||
conditions []Condition
|
||||
prefix string
|
||||
condition Condition
|
||||
}
|
||||
|
||||
// New creates a new query.
|
||||
func New(prefix string, conditions ...Condition) (*Query, error) {
|
||||
func New(prefix string, condition Condition) (*Query, error) {
|
||||
// check prefix
|
||||
if !prefixExpr.MatchString(prefix) {
|
||||
return nil, fmt.Errorf("invalid prefix: %s", prefix)
|
||||
}
|
||||
|
||||
// check conditions
|
||||
var err error
|
||||
for _, cond := range conditions {
|
||||
err = cond.check()
|
||||
// check condition
|
||||
if condition != nil {
|
||||
err := condition.check()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
condition = &noCond{}
|
||||
}
|
||||
|
||||
// return query
|
||||
return &Query{
|
||||
prefix: prefix,
|
||||
conditions: conditions,
|
||||
prefix: prefix,
|
||||
condition: condition,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MustCompile creates a new query and panics on an error.
|
||||
func MustCompile(prefix string, conditions ...Condition) *Query {
|
||||
q, err := New(prefix, conditions...)
|
||||
func MustCompile(prefix string, condition Condition) *Query {
|
||||
q, err := New(prefix, condition)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
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.
|
||||
func (q *Query) Matches(f Fetcher) bool {
|
||||
for _, cond := range q.conditions {
|
||||
if !cond.complies(f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return q.condition.complies(f)
|
||||
}
|
||||
|
||||
// String returns the string representation of the query.
|
||||
func (q *Query) String() string {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -45,18 +45,16 @@ var (
|
|||
)
|
||||
|
||||
func testQuery(t *testing.T, f Fetcher, shouldMatch bool, condition Condition) {
|
||||
q := MustCompile("test:")
|
||||
err := q.Append(condition)
|
||||
if err != nil {
|
||||
t.Errorf("append failed: %s", err)
|
||||
}
|
||||
q := MustCompile("test:", condition)
|
||||
|
||||
// fmt.Printf("%s\n", q.String())
|
||||
|
||||
matched := q.Matches(f)
|
||||
switch {
|
||||
case matched && !shouldMatch:
|
||||
t.Errorf("query should match")
|
||||
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, "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", StartsWith, "fin"))
|
||||
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, "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, And(
|
||||
Where("happy", Is, "1"),
|
||||
Not(Or(
|
||||
Where("happy", Is, false),
|
||||
Where("happy", Is, "f"),
|
||||
)),
|
||||
))
|
||||
|
||||
testQuery(t, f, true, Where("happy", Exists, nil))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue