mirror of
https://github.com/safing/portbase
synced 2025-09-10 06:15:08 +00:00
Finish query package for now
This commit is contained in:
parent
115b18dfb6
commit
e40d66e103
15 changed files with 714 additions and 181 deletions
|
@ -4,77 +4,122 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
operatorNames = map[string]uint8{
|
||||
"==": Equals,
|
||||
">": GreaterThan,
|
||||
">=": GreaterThanOrEqual,
|
||||
"<": LessThan,
|
||||
"<=": LessThanOrEqual,
|
||||
"f==": FloatEquals,
|
||||
"f>": FloatGreaterThan,
|
||||
"f>=": FloatGreaterThanOrEqual,
|
||||
"f<": FloatLessThan,
|
||||
"f<=": FloatLessThanOrEqual,
|
||||
"sameas": SameAs,
|
||||
"s==": SameAs,
|
||||
"contains": Contains,
|
||||
"co": Contains,
|
||||
"startswith": StartsWith,
|
||||
"sw": StartsWith,
|
||||
"endswith": EndsWith,
|
||||
"ew": EndsWith,
|
||||
"in": In,
|
||||
"matches": Matches,
|
||||
"re": Matches,
|
||||
"is": Is,
|
||||
"exists": Exists,
|
||||
"ex": Exists,
|
||||
}
|
||||
|
||||
primaryNames = make(map[uint8]string)
|
||||
)
|
||||
|
||||
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]"
|
||||
}
|
||||
|
||||
type treeElement struct {
|
||||
branches []*treeElement
|
||||
type snippet struct {
|
||||
text string
|
||||
globalPosition int
|
||||
}
|
||||
|
||||
var (
|
||||
escapeReplacer = regexp.MustCompile("\\\\([^\\\\])")
|
||||
)
|
||||
// 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
|
||||
|
||||
// prepToken removes surrounding parenthesis and escape characters.
|
||||
func prepToken(text string) string {
|
||||
return escapeReplacer.ReplaceAllString(strings.Trim(text, "\""), "$1")
|
||||
getSnippet := func() (*snippet, error) {
|
||||
// order is important, as parseAndOr will always consume one additional snippet.
|
||||
snippetsPos++
|
||||
if snippetsPos > len(snippets) {
|
||||
return nil, fmt.Errorf("unexpected end at position %d", len(query))
|
||||
}
|
||||
return snippets[snippetsPos-1], nil
|
||||
}
|
||||
remainingSnippets := func() int {
|
||||
return len(snippets) - snippetsPos
|
||||
}
|
||||
|
||||
// check for query word
|
||||
queryWord, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if queryWord.text != "query" {
|
||||
return nil, errors.New("queries must start with \"query\"")
|
||||
}
|
||||
|
||||
// get prefix
|
||||
prefix, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := New(prefix.text)
|
||||
|
||||
for remainingSnippets() > 0 {
|
||||
command, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch command.text {
|
||||
case "where":
|
||||
if q.where != nil {
|
||||
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
||||
}
|
||||
|
||||
// parse conditions
|
||||
condition, err := parseAndOr(getSnippet, remainingSnippets, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// go one back, as parseAndOr had to check if its done
|
||||
snippetsPos--
|
||||
|
||||
q.Where(condition)
|
||||
case "orderby":
|
||||
if q.orderBy != "" {
|
||||
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
||||
}
|
||||
|
||||
orderBySnippet, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.OrderBy(orderBySnippet.text)
|
||||
case "limit":
|
||||
if q.limit != 0 {
|
||||
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
||||
}
|
||||
|
||||
limitSnippet, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limit, err := strconv.ParseUint(limitSnippet.text, 10, 31)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse integer (%s) at position %d", limitSnippet.text, limitSnippet.globalPosition)
|
||||
}
|
||||
|
||||
q.Limit(int(limit))
|
||||
case "offset":
|
||||
if q.offset != 0 {
|
||||
return nil, fmt.Errorf("duplicate \"%s\" clause found at position %d", command.text, command.globalPosition)
|
||||
}
|
||||
|
||||
offsetSnippet, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset, err := strconv.ParseUint(offsetSnippet.text, 10, 31)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse integer (%s) at position %d", offsetSnippet.text, offsetSnippet.globalPosition)
|
||||
}
|
||||
|
||||
q.Offset(int(offset))
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown clause \"%s\" at position %d", command.text, command.globalPosition)
|
||||
}
|
||||
}
|
||||
|
||||
return q.Check()
|
||||
}
|
||||
|
||||
func extractSnippets(text string) (snippets []*treeElement, err error) {
|
||||
func extractSnippets(text string) (snippets []*snippet, err error) {
|
||||
|
||||
skip := false
|
||||
start := -1
|
||||
|
@ -93,10 +138,10 @@ func extractSnippets(text string) (snippets []*treeElement, err error) {
|
|||
skip = true
|
||||
}
|
||||
|
||||
// wait for parenthesis to be over
|
||||
// wait for parenthesis to be overs
|
||||
if inParenthesis {
|
||||
if char == '"' {
|
||||
snippets = append(snippets, &treeElement{
|
||||
snippets = append(snippets, &snippet{
|
||||
text: prepToken(text[start+1 : pos]),
|
||||
globalPosition: start + 1,
|
||||
})
|
||||
|
@ -110,9 +155,9 @@ func extractSnippets(text string) (snippets []*treeElement, err error) {
|
|||
switch char {
|
||||
case '\t', '\n', '\r', ' ', '(', ')':
|
||||
if start >= 0 {
|
||||
snippets = append(snippets, &treeElement{
|
||||
snippets = append(snippets, &snippet{
|
||||
text: prepToken(text[start:pos]),
|
||||
globalPosition: start,
|
||||
globalPosition: start + 1,
|
||||
})
|
||||
start = -1
|
||||
}
|
||||
|
@ -125,13 +170,13 @@ func extractSnippets(text string) (snippets []*treeElement, err error) {
|
|||
// handle special segment characters
|
||||
switch char {
|
||||
case '(', ')':
|
||||
snippets = append(snippets, &treeElement{
|
||||
snippets = append(snippets, &snippet{
|
||||
text: text[pos : pos+1],
|
||||
globalPosition: pos,
|
||||
globalPosition: pos + 1,
|
||||
})
|
||||
case '"':
|
||||
if start < pos {
|
||||
return nil, fmt.Errorf("parenthesis ('\"') may not be within words, please escape with '\\' (position: %d)", pos+1)
|
||||
return nil, fmt.Errorf("parenthesis ('\"') may not be used within words, please escape with '\\' (position: %d)", pos+1)
|
||||
}
|
||||
inParenthesis = true
|
||||
}
|
||||
|
@ -139,89 +184,166 @@ func extractSnippets(text string) (snippets []*treeElement, err error) {
|
|||
}
|
||||
|
||||
// add last
|
||||
snippets = append(snippets, &treeElement{
|
||||
text: prepToken(text[start : pos+1]),
|
||||
globalPosition: start,
|
||||
})
|
||||
if start >= 0 {
|
||||
snippets = append(snippets, &snippet{
|
||||
text: prepToken(text[start : pos+1]),
|
||||
globalPosition: start + 1,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
func parseAndOr(getSnippet func() (*snippet, error), remainingSnippets func() int, rootCondition bool) (Condition, error) {
|
||||
var isOr = false
|
||||
var typeSet = false
|
||||
var wrapInNot = false
|
||||
var expectingMore = true
|
||||
var conditions []Condition
|
||||
|
||||
getElement := func() (*treeElement, error) {
|
||||
if snippetsPos >= len(snippets) {
|
||||
return nil, fmt.Errorf("unexpected end at position %d", len(query))
|
||||
for {
|
||||
if !expectingMore && rootCondition && remainingSnippets() == 0 {
|
||||
// advance snippetsPos by one, as it will be set back by 1
|
||||
getSnippet()
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0], nil
|
||||
}
|
||||
if isOr {
|
||||
return Or(conditions...), nil
|
||||
}
|
||||
return And(conditions...), nil
|
||||
}
|
||||
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\"")
|
||||
}
|
||||
firstSnippet, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get prefix
|
||||
prefix, err := getElement()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !expectingMore && rootCondition {
|
||||
switch firstSnippet.text {
|
||||
case "orderby", "limit", "offset":
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0], nil
|
||||
}
|
||||
if isOr {
|
||||
return Or(conditions...), nil
|
||||
}
|
||||
return And(conditions...), nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if no condition
|
||||
if len(snippets) == 2 {
|
||||
return New(prefix.text, nil)
|
||||
switch firstSnippet.text {
|
||||
case "(":
|
||||
condition, err := parseAndOr(getSnippet, remainingSnippets, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wrapInNot {
|
||||
conditions = append(conditions, Not(condition))
|
||||
wrapInNot = false
|
||||
} else {
|
||||
conditions = append(conditions, condition)
|
||||
}
|
||||
expectingMore = true
|
||||
case ")":
|
||||
if len(conditions) == 1 {
|
||||
return conditions[0], nil
|
||||
}
|
||||
if isOr {
|
||||
return Or(conditions...), nil
|
||||
}
|
||||
return And(conditions...), nil
|
||||
case "and":
|
||||
if typeSet && isOr {
|
||||
return nil, fmt.Errorf("you may not mix \"and\" and \"or\" (position: %d)", firstSnippet.globalPosition)
|
||||
}
|
||||
isOr = false
|
||||
typeSet = true
|
||||
expectingMore = true
|
||||
case "or":
|
||||
if typeSet && !isOr {
|
||||
return nil, fmt.Errorf("you may not mix \"and\" and \"or\" (position: %d)", firstSnippet.globalPosition)
|
||||
}
|
||||
isOr = true
|
||||
typeSet = true
|
||||
expectingMore = true
|
||||
case "not":
|
||||
wrapInNot = true
|
||||
expectingMore = true
|
||||
default:
|
||||
condition, err := parseCondition(firstSnippet, getSnippet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wrapInNot {
|
||||
conditions = append(conditions, Not(condition))
|
||||
wrapInNot = false
|
||||
} else {
|
||||
conditions = append(conditions, condition)
|
||||
}
|
||||
expectingMore = false
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
func parseCondition(firstSnippet *snippet, getSnippet func() (*snippet, error)) (Condition, error) {
|
||||
wrapInNot := false
|
||||
|
||||
// get operator name
|
||||
opName, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch first.text {
|
||||
case "(":
|
||||
return parseAndOr(getElement, true, nil, false)
|
||||
// case ""
|
||||
// negate?
|
||||
if opName.text == "not" {
|
||||
wrapInNot = true
|
||||
opName, err = getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
// get operator
|
||||
operator, ok := operatorNames[opName.text]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown operator at position %d", opName.globalPosition)
|
||||
}
|
||||
|
||||
// don't need a value for "exists"
|
||||
if operator == Exists {
|
||||
if wrapInNot {
|
||||
return Not(Where(firstSnippet.text, operator, nil)), nil
|
||||
}
|
||||
return Where(firstSnippet.text, operator, nil), nil
|
||||
}
|
||||
|
||||
// get value
|
||||
value, err := getSnippet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wrapInNot {
|
||||
return Not(Where(firstSnippet.text, operator, value.text)), nil
|
||||
}
|
||||
return Where(firstSnippet.text, operator, value.text), nil
|
||||
}
|
||||
|
||||
func parseAndOr(getElement func() (*treeElement, error), expectBracket bool, preParsedCondition Condition, preParsedIsOr bool) (Condition, error) {
|
||||
return nil, nil
|
||||
var (
|
||||
escapeReplacer = regexp.MustCompile("\\\\([^\\\\])")
|
||||
)
|
||||
|
||||
// prepToken removes surrounding parenthesis and escape characters.
|
||||
func prepToken(text string) string {
|
||||
return escapeReplacer.ReplaceAllString(strings.Trim(text, "\""), "$1")
|
||||
}
|
||||
|
||||
// escapeString correctly escapes a snippet for printing
|
||||
func escapeString(token string) string {
|
||||
// check if token contains characters that need to be escaped
|
||||
if strings.ContainsAny(token, "()\"\\\t\r\n ") {
|
||||
// put the token in parenthesis and only escape \ and "
|
||||
return fmt.Sprintf("\"%s\"", strings.Replace(token, "\"", "\\\"", -1))
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue