chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kashav/fsql/query"
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
var allAttributes = []string{"mode", "size", "time", "hash", "name"}
|
||||
|
||||
func isValidAttribute(attribute string) error {
|
||||
for _, valid := range allAttributes {
|
||||
if attribute == valid {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &ErrUnknownToken{attribute}
|
||||
}
|
||||
|
||||
// parseAttrs parses the list of attributes passed to the SELECT clause.
|
||||
func (p *parser) parseAttrs(attributes *[]string, modifiers *map[string][]query.Modifier) error {
|
||||
for {
|
||||
ident := p.expect(tokenizer.Identifier)
|
||||
if ident == nil {
|
||||
return p.currentError()
|
||||
}
|
||||
|
||||
if ident.Raw == "*" || ident.Raw == "all" {
|
||||
*attributes = allAttributes
|
||||
} else {
|
||||
p.current = ident
|
||||
|
||||
attrModifiers := make([]query.Modifier, 0)
|
||||
attribute, err := p.parseAttr(&attrModifiers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*attributes = append(*attributes, attribute.Raw)
|
||||
(*modifiers)[attribute.Raw] = attrModifiers
|
||||
}
|
||||
|
||||
if p.expect(tokenizer.Comma) == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAttr recursively parses an attribute's modifiers and returns the
|
||||
// associated attribute.
|
||||
func (p *parser) parseAttr(modifiers *[]query.Modifier) (*tokenizer.Token, error) {
|
||||
ident := p.expect(tokenizer.Identifier)
|
||||
if ident == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
|
||||
// ident is a modifier name (e.g. `FORMAT`) iff the next token is an open
|
||||
// paren, otherwise an attribute (e.g. `name`).
|
||||
if token := p.expect(tokenizer.OpenParen); token == nil {
|
||||
if err := isValidAttribute(ident.Raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
// In the case of chained modifiers, we want to recurse and parse each
|
||||
// inner modifier first. parseAttribute returns the associated attribute that
|
||||
// we're looking for.
|
||||
attribute, err := p.parseAttr(modifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if attribute == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
if err := isValidAttribute(attribute.Raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modifier := query.Modifier{
|
||||
Name: strings.ToUpper(ident.Raw),
|
||||
Arguments: make([]string, 0),
|
||||
}
|
||||
|
||||
// Parse the modifier arguments.
|
||||
for {
|
||||
if token := p.expect(tokenizer.Identifier); token != nil {
|
||||
modifier.Arguments = append(modifier.Arguments, token.Raw)
|
||||
continue
|
||||
}
|
||||
|
||||
if token := p.expect(tokenizer.Comma); token != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if token := p.expect(tokenizer.CloseParen); token != nil {
|
||||
*modifiers = append(*modifiers, modifier)
|
||||
return attribute, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kashav/fsql/query"
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
func TestAttributeParser_ExpectCorrectAttributes(t *testing.T) {
|
||||
type Expected struct {
|
||||
attributes []string
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "name",
|
||||
expected: Expected{attributes: []string{"name"}, err: nil},
|
||||
},
|
||||
{
|
||||
input: "name, size",
|
||||
expected: Expected{
|
||||
attributes: []string{"name", "size"},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "*",
|
||||
expected: Expected{attributes: allAttributes, err: nil},
|
||||
},
|
||||
{
|
||||
input: "all",
|
||||
expected: Expected{attributes: allAttributes, err: nil},
|
||||
},
|
||||
{
|
||||
input: "format(time, iso)",
|
||||
expected: Expected{attributes: []string{"time"}, err: nil},
|
||||
},
|
||||
|
||||
{
|
||||
input: "",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
{
|
||||
input: "name,",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
{
|
||||
input: "identifier",
|
||||
expected: Expected{err: &ErrUnknownToken{"identifier"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
attributes := make([]string, 0)
|
||||
modifiers := make(map[string][]query.Modifier)
|
||||
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(c.input)}
|
||||
err := p.parseAttrs(&attributes, &modifiers)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.attributes, attributes) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.attributes, attributes)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributeParser_ExpectCorrectModifiers(t *testing.T) {
|
||||
type Expected struct {
|
||||
modifiers map[string][]query.Modifier
|
||||
err error
|
||||
}
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "name",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{"name": {}},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "upper(name)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"name": {
|
||||
{
|
||||
Name: "UPPER",
|
||||
Arguments: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "format(time, iso)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"time": {
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"iso"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "format(time, \"iso\")",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"time": {
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"iso"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "lower(name), format(size, mb)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"name": {
|
||||
{
|
||||
Name: "LOWER",
|
||||
Arguments: []string{},
|
||||
},
|
||||
},
|
||||
"size": {
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"mb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "format(fullpath(name), lower)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"name": {
|
||||
{
|
||||
Name: "FULLPATH",
|
||||
Arguments: []string{},
|
||||
},
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"lower"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
// No function/parameter validation yet!
|
||||
{
|
||||
input: "foo(name)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"name": {{
|
||||
Name: "FOO",
|
||||
Arguments: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "format(size, tb)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"size": {
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"tb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "format(size, kb, mb)",
|
||||
expected: Expected{
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"size": {
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"kb", "mb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
{
|
||||
input: "lower(name),",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
{
|
||||
input: "identifier",
|
||||
expected: Expected{err: &ErrUnknownToken{"identifier"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
attributes := make([]string, 0)
|
||||
modifiers := make(map[string][]query.Modifier)
|
||||
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(c.input)}
|
||||
err := p.parseAttrs(&attributes, &modifiers)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.modifiers, modifiers) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.modifiers, modifiers)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/oleiade/lane"
|
||||
|
||||
"github.com/kashav/fsql/query"
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
// parseConditionTree parses the condition tree passed to the WHERE clause.
|
||||
func (p *parser) parseConditionTree() (*query.ConditionNode, error) {
|
||||
stack := lane.NewStack()
|
||||
errFailedToParse := errors.New("failed to parse conditions")
|
||||
|
||||
for {
|
||||
if p.current = p.tokenizer.Next(); p.current == nil {
|
||||
break
|
||||
}
|
||||
|
||||
switch p.current.Type {
|
||||
|
||||
case tokenizer.Not:
|
||||
// TODO: Handle NOT (...), for the time being we proceed with the other
|
||||
// tokens and handle the negation when parsing the condition.
|
||||
fallthrough
|
||||
|
||||
case tokenizer.Identifier:
|
||||
condition, err := p.parseCondition()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if condition == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
|
||||
if condition.IsSubquery {
|
||||
if err := p.parseSubquery(condition); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
leafNode := &query.ConditionNode{Condition: condition}
|
||||
if prevNode, ok := stack.Pop().(*query.ConditionNode); !ok {
|
||||
stack.Push(leafNode)
|
||||
} else if prevNode.Condition == nil {
|
||||
prevNode.Right = leafNode
|
||||
stack.Push(prevNode)
|
||||
} else {
|
||||
return nil, errFailedToParse
|
||||
}
|
||||
|
||||
case tokenizer.And, tokenizer.Or:
|
||||
leftNode, ok := stack.Pop().(*query.ConditionNode)
|
||||
if !ok {
|
||||
return nil, errFailedToParse
|
||||
}
|
||||
|
||||
node := query.ConditionNode{
|
||||
Type: &p.current.Type,
|
||||
Left: leftNode,
|
||||
}
|
||||
stack.Push(&node)
|
||||
|
||||
case tokenizer.OpenParen:
|
||||
stack.Push(nil)
|
||||
|
||||
case tokenizer.CloseParen:
|
||||
rightNode, ok := stack.Pop().(*query.ConditionNode)
|
||||
if !ok {
|
||||
return nil, errFailedToParse
|
||||
}
|
||||
|
||||
if rootNode, ok := stack.Pop().(*query.ConditionNode); !ok {
|
||||
stack.Push(rightNode)
|
||||
} else {
|
||||
rootNode.Right = rightNode
|
||||
stack.Push(rootNode)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if stack.Size() == 0 {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
|
||||
if stack.Size() > 1 {
|
||||
return nil, errFailedToParse
|
||||
}
|
||||
|
||||
node, ok := stack.Pop().(*query.ConditionNode)
|
||||
if !ok {
|
||||
return nil, errFailedToParse
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// parseCondition parses and returns the next condition.
|
||||
func (p *parser) parseCondition() (*query.Condition, error) {
|
||||
cond := &query.Condition{}
|
||||
|
||||
// If we find a NOT, negate the condition.
|
||||
if p.expect(tokenizer.Not) != nil {
|
||||
cond.Negate = true
|
||||
}
|
||||
|
||||
ident := p.expect(tokenizer.Identifier)
|
||||
if ident == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
p.current = ident
|
||||
|
||||
var modifiers []query.Modifier
|
||||
attr, err := p.parseAttr(&modifiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond.Attribute = attr.Raw
|
||||
cond.AttributeModifiers = modifiers
|
||||
|
||||
// If this condition has modifiers, then p.current was unset while parsing
|
||||
// the modifier, se we set the current token manually.
|
||||
if len(modifiers) > 0 {
|
||||
p.current = p.tokenizer.Next()
|
||||
}
|
||||
if p.current == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
cond.Operator = p.current.Type
|
||||
p.current = nil
|
||||
|
||||
// Parse subquery of format `(...)`.
|
||||
if p.expect(tokenizer.OpenParen) != nil {
|
||||
token := p.expect(tokenizer.Subquery)
|
||||
if token == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
cond.IsSubquery = true
|
||||
cond.Value = token.Raw
|
||||
if p.expect(tokenizer.CloseParen) == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
// Parse list of values of format `[...]`.
|
||||
if p.expect(tokenizer.OpenBracket) != nil {
|
||||
values := make([]string, 0)
|
||||
for {
|
||||
if token := p.expect(tokenizer.Identifier); token != nil {
|
||||
values = append(values, token.Raw)
|
||||
}
|
||||
if p.expect(tokenizer.Comma) != nil {
|
||||
continue
|
||||
}
|
||||
if p.expect(tokenizer.CloseBracket) != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
cond.Value = values
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
// Not a list nor a subquery -> plain identifier!
|
||||
token := p.expect(tokenizer.Identifier)
|
||||
if token == nil {
|
||||
return nil, p.currentError()
|
||||
}
|
||||
cond.Value = token.Raw
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
// parseSubquery parses a subquery by recursively evaluating it's condition(s).
|
||||
// If the subquery contains references to aliases from the superquery, it's
|
||||
// Subquery attribute is set. Otherwise, we evaluate it's Subquery and set
|
||||
// it's Value to the result.
|
||||
func (p *parser) parseSubquery(condition *query.Condition) error {
|
||||
q, err := Run(condition.Value.(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the subquery has aliases, we'll have to parse the subquery against
|
||||
// each file, so we don't do anything here.
|
||||
if len(q.SourceAliases) > 0 {
|
||||
condition.Subquery = q
|
||||
return nil
|
||||
}
|
||||
|
||||
value := make(map[interface{}]bool, 0)
|
||||
workFunc := func(path string, info os.FileInfo, res map[string]interface{}) {
|
||||
for _, attr := range [...]string{"name", "size", "time", "mode"} {
|
||||
if q.HasAttribute(attr) {
|
||||
value[res[attr]] = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = q.Execute(workFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
condition.Value = value
|
||||
condition.IsSubquery = false
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kashav/fsql/query"
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
func TestConditionParser_ExpectCorrectCondition(t *testing.T) {
|
||||
type Expected struct {
|
||||
condition *query.Condition
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "name LIKE foo%",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "foo%",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "size = 10",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "size",
|
||||
Operator: tokenizer.Equals,
|
||||
Value: "10",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "mode IS dir",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "mode",
|
||||
Operator: tokenizer.Is,
|
||||
Value: "dir",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "format(time, iso) >= 2017-05-28T16:37:18Z",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "time",
|
||||
AttributeModifiers: []query.Modifier{
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"iso"},
|
||||
},
|
||||
},
|
||||
Operator: tokenizer.GreaterThanEquals,
|
||||
Value: "2017-05-28T16:37:18Z",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "upper(name) != FOO",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
AttributeModifiers: []query.Modifier{
|
||||
{
|
||||
Name: "UPPER",
|
||||
Arguments: []string{},
|
||||
},
|
||||
},
|
||||
Operator: tokenizer.NotEquals,
|
||||
Value: "FOO",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "NOT name IN [foo,bar,baz]",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.In,
|
||||
Value: []string{"foo", "bar", "baz"},
|
||||
Negate: true,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
// No attribute-operator validation yet (these 3 should /eventually/ throw
|
||||
// some error)!
|
||||
{
|
||||
input: "time RLIKE '.*'",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "time",
|
||||
Operator: tokenizer.RLike,
|
||||
Value: ".*",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "size LIKE foo",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "size",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "foo",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "time <> now",
|
||||
expected: Expected{
|
||||
condition: &query.Condition{
|
||||
Attribute: "time",
|
||||
Operator: tokenizer.NotEquals,
|
||||
Value: "now",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "name =",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
|
||||
{
|
||||
input: "file IS dir",
|
||||
expected: Expected{err: &ErrUnknownToken{"file"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(c.input)}
|
||||
actual, err := p.parseCondition()
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.condition, actual) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.condition, actual)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionParser_ExpectCorrectConditionTree(t *testing.T) {
|
||||
type Expected struct {
|
||||
node *query.ConditionNode
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
// Not sure why, but compiler throws when attempting to take the memory
|
||||
// address of any tokenizer.TokenType.
|
||||
var (
|
||||
tmpAnd = tokenizer.And
|
||||
tmpOr = tokenizer.Or
|
||||
)
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "name LIKE foo%",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "foo%",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "upper(name) = MAIN",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
AttributeModifiers: []query.Modifier{
|
||||
{
|
||||
Name: "UPPER",
|
||||
Arguments: []string{},
|
||||
},
|
||||
},
|
||||
Operator: tokenizer.Equals,
|
||||
Value: "MAIN",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "name LIKE %foo AND name <> bar.foo",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{
|
||||
Type: &tmpAnd,
|
||||
Left: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "%foo",
|
||||
},
|
||||
},
|
||||
Right: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.NotEquals,
|
||||
Value: "bar.foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "size <= 10 OR NOT mode IS dir",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{
|
||||
Type: &tmpOr,
|
||||
Left: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "size",
|
||||
Operator: tokenizer.LessThanEquals,
|
||||
Value: "10",
|
||||
},
|
||||
},
|
||||
Right: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "mode",
|
||||
Operator: tokenizer.Is,
|
||||
Value: "dir",
|
||||
Negate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "size = 5 AND name = foo",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{
|
||||
Type: &tmpAnd,
|
||||
Left: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "size",
|
||||
Operator: tokenizer.Equals,
|
||||
Value: "5",
|
||||
},
|
||||
},
|
||||
Right: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Equals,
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "format(size, mb) <= 2 AND (name = foo OR name = bar)",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{
|
||||
Type: &tmpAnd,
|
||||
Left: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "size",
|
||||
AttributeModifiers: []query.Modifier{
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"mb"},
|
||||
},
|
||||
},
|
||||
Operator: tokenizer.LessThanEquals,
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
Right: &query.ConditionNode{
|
||||
Type: &tmpOr,
|
||||
Left: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Equals,
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
Right: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Equals,
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "name = foo AND NOT (name = bar OR name = baz)",
|
||||
expected: Expected{
|
||||
node: &query.ConditionNode{},
|
||||
err: &ErrUnexpectedToken{
|
||||
Expected: tokenizer.Identifier,
|
||||
Actual: tokenizer.OpenParen,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "size = 5 AND ()",
|
||||
expected: Expected{err: errors.New("failed to parse conditions")},
|
||||
},
|
||||
|
||||
// FIXME: The following case /should/ throw EOF (it doesn't right now).
|
||||
// Case{input: "name = foo AND", expected: Expected{err: io.ErrUnexpectedEOF}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(c.input)}
|
||||
actual, err := p.parseConditionTree()
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.node, actual) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.node, actual)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionParser_ExpectCorrectSubquery(t *testing.T) {
|
||||
type Expected struct {
|
||||
condition *query.Condition
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input *query.Condition
|
||||
expected Expected
|
||||
}
|
||||
|
||||
// TODO: Complete these cases. This test relies on testdata fixtures.
|
||||
cases := []Case{}
|
||||
|
||||
for _, c := range cases {
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer("")}
|
||||
err := p.parseSubquery(c.input)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.condition, c.input) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.condition, c.input)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
// ErrUnexpectedToken represents an unexpected token error.
|
||||
type ErrUnexpectedToken struct {
|
||||
Actual tokenizer.TokenType
|
||||
Expected tokenizer.TokenType
|
||||
}
|
||||
|
||||
func (e *ErrUnexpectedToken) Error() string {
|
||||
return fmt.Sprintf("expected %s; got %s", e.Expected.String(),
|
||||
e.Actual.String())
|
||||
}
|
||||
|
||||
// ErrUnknownToken represents an unknown token error.
|
||||
type ErrUnknownToken struct {
|
||||
Raw string
|
||||
}
|
||||
|
||||
func (e *ErrUnknownToken) Error() string {
|
||||
return fmt.Sprintf("unknown token: %s", e.Raw)
|
||||
}
|
||||
|
||||
// currentError returns the current error, based on the parser's current Token
|
||||
// and the previously expected TokenType (set in parser.expect).
|
||||
func (p *parser) currentError() error {
|
||||
if p.current == nil {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
if p.current.Type == tokenizer.Unknown {
|
||||
return &ErrUnknownToken{Raw: p.current.Raw}
|
||||
}
|
||||
|
||||
return &ErrUnexpectedToken{Actual: p.current.Type, Expected: p.expected}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
func TestParser_ErrUnexpectedToken(t *testing.T) {
|
||||
err := &ErrUnexpectedToken{
|
||||
Actual: tokenizer.Select,
|
||||
Expected: tokenizer.Where,
|
||||
}
|
||||
expected := "expected where; got select"
|
||||
actual := err.Error()
|
||||
if expected != actual {
|
||||
t.Fatalf("\nExpected: %s\n Got: %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ErrUnknownTokent(t *testing.T) {
|
||||
err := &ErrUnknownToken{"r"}
|
||||
expected := "unknown token: r"
|
||||
actual := err.Error()
|
||||
if expected != actual {
|
||||
t.Fatalf("\nExpected: %s\n Got: %s", expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kashav/fsql/query"
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
// Run parses the input string and returns the parsed AST (query).
|
||||
func Run(input string) (*query.Query, error) {
|
||||
return (&parser{}).parse(input)
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
tokenizer *tokenizer.Tokenizer
|
||||
current *tokenizer.Token
|
||||
expected tokenizer.TokenType
|
||||
}
|
||||
|
||||
// parse runs the respective parser function on each clause of the query.
|
||||
func (p *parser) parse(input string) (*query.Query, error) {
|
||||
q := query.NewQuery()
|
||||
p.tokenizer = tokenizer.NewTokenizer(input)
|
||||
if err := p.parseSelectClause(q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.parseFromClause(q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.parseWhereClause(q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// parseSelectClause parses the SELECT clause of the query.
|
||||
func (p *parser) parseSelectClause(q *query.Query) error {
|
||||
// Determine if we should show all attributes. This is only true when
|
||||
// no attributes are provided (regardless of if the SELECT keyword is
|
||||
// provided or not).
|
||||
var showAll = true
|
||||
if p.expect(tokenizer.Select) == nil {
|
||||
if p.current == nil || p.current.Type == tokenizer.Identifier {
|
||||
showAll = false
|
||||
} else if p.current.Type == tokenizer.From || p.current.Type == tokenizer.Where {
|
||||
// No SELECT and next token is FROM/WHERE, show all!
|
||||
showAll = true
|
||||
} else {
|
||||
// No SELECT and next token is not Identifier nor FROM/WHERE -> malformed
|
||||
// input.
|
||||
return p.currentError()
|
||||
}
|
||||
} else if current := p.expect(tokenizer.Identifier); current != nil {
|
||||
p.current = current
|
||||
showAll = false
|
||||
}
|
||||
|
||||
if showAll {
|
||||
q.Attributes = allAttributes
|
||||
} else if err := p.parseAttrs(&q.Attributes, &q.Modifiers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseFromClause parses the FROM clause of the query.
|
||||
func (p *parser) parseFromClause(q *query.Query) error {
|
||||
if p.expect(tokenizer.From) == nil {
|
||||
err := p.currentError()
|
||||
if p.expect(tokenizer.Identifier) != nil {
|
||||
// No FROM, but an identifier -> malformed query.
|
||||
return err
|
||||
}
|
||||
|
||||
// No specified directory, so we default to the CWD.
|
||||
q.Sources["include"] = append(q.Sources["include"], ".")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := p.parseSourceList(&q.Sources, &q.SourceAliases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace the tilde with the home directory in each source directory. This
|
||||
// is only required when the query is wrapped in quotes, since the shell
|
||||
// will automatically expand tildes otherwise.
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sourceType := range []string{"include", "exclude"} {
|
||||
for i, src := range q.Sources[sourceType] {
|
||||
if strings.Contains(src, "~") {
|
||||
q.Sources[sourceType][i] = filepath.Join(u.HomeDir, src[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseWhereClause parses the WHERE clause of the query.
|
||||
func (p *parser) parseWhereClause(q *query.Query) error {
|
||||
if p.expect(tokenizer.Where) == nil {
|
||||
err := p.currentError()
|
||||
if p.expect(tokenizer.Identifier) == nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
root, err := p.parseConditionTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.ConditionTree = root
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expect returns the next token if it matches the expectation t, and
|
||||
// nil otherwise.
|
||||
func (p *parser) expect(t tokenizer.TokenType) *tokenizer.Token {
|
||||
p.expected = t
|
||||
|
||||
if p.current == nil {
|
||||
p.current = p.tokenizer.Next()
|
||||
}
|
||||
|
||||
if p.current != nil && p.current.Type == t {
|
||||
tok := p.current
|
||||
p.current = nil
|
||||
return tok
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kashav/fsql/query"
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
func TestParser_ParseSelect(t *testing.T) {
|
||||
type Expected struct {
|
||||
attributes []string
|
||||
modifiers map[string][]query.Modifier
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "all",
|
||||
expected: Expected{
|
||||
attributes: allAttributes,
|
||||
modifiers: map[string][]query.Modifier{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "SELECT",
|
||||
expected: Expected{
|
||||
attributes: allAttributes,
|
||||
modifiers: map[string][]query.Modifier{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "FROM",
|
||||
expected: Expected{
|
||||
attributes: allAttributes,
|
||||
modifiers: map[string][]query.Modifier{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "SELECT name",
|
||||
expected: Expected{
|
||||
attributes: []string{"name"},
|
||||
modifiers: map[string][]query.Modifier{"name": {}},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "SELECT format(size, kb)",
|
||||
expected: Expected{
|
||||
attributes: []string{"size"},
|
||||
modifiers: map[string][]query.Modifier{
|
||||
"size": {
|
||||
{
|
||||
Name: "FORMAT",
|
||||
Arguments: []string{"kb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
q := query.NewQuery()
|
||||
err := (&parser{tokenizer: tokenizer.NewTokenizer(c.input)}).parseSelectClause(q)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.attributes, q.Attributes) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.attributes, q.Attributes)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.modifiers, q.Modifiers) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.modifiers, q.Modifiers)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseFrom(t *testing.T) {
|
||||
type Expected struct {
|
||||
sources map[string][]string
|
||||
aliases map[string]string
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
// TODO: If we can't get the current user, should we fatal or just return?
|
||||
return
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "WHERE",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{
|
||||
"include": {"."},
|
||||
"exclude": {},
|
||||
},
|
||||
aliases: map[string]string{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "FROM .",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{
|
||||
"include": {"."},
|
||||
"exclude": {},
|
||||
},
|
||||
aliases: map[string]string{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "FROM ~/foo, -./.git/",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{
|
||||
"include": {u.HomeDir + "/foo"},
|
||||
"exclude": {".git"},
|
||||
},
|
||||
aliases: map[string]string{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "FROM ./foo/ AS foo",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{
|
||||
"include": {"foo"},
|
||||
"exclude": {},
|
||||
},
|
||||
aliases: map[string]string{"foo": "foo"},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "FROM",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
|
||||
{
|
||||
input: "FROM WHERE",
|
||||
expected: Expected{
|
||||
err: &ErrUnexpectedToken{
|
||||
Actual: tokenizer.Where,
|
||||
Expected: tokenizer.Identifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
q := query.NewQuery()
|
||||
err := (&parser{tokenizer: tokenizer.NewTokenizer(c.input)}).parseFromClause(q)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.sources, q.Sources) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.sources, q.Sources)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.aliases, q.SourceAliases) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.aliases, q.SourceAliases)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseWhere(t *testing.T) {
|
||||
type Expected struct {
|
||||
tree *query.ConditionNode
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: "WHERE name LIKE foo",
|
||||
expected: Expected{
|
||||
tree: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
// Our tree is fully-zeroed in this case, so it's easier just to give it
|
||||
// an empty Expected struct.
|
||||
{input: "", expected: Expected{}},
|
||||
|
||||
{input: "WHERE", expected: Expected{err: io.ErrUnexpectedEOF}},
|
||||
|
||||
{
|
||||
input: "name LIKE foo",
|
||||
expected: Expected{
|
||||
err: &ErrUnexpectedToken{
|
||||
Expected: tokenizer.Where,
|
||||
Actual: tokenizer.Identifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
q := query.NewQuery()
|
||||
err := (&parser{tokenizer: tokenizer.NewTokenizer(c.input)}).parseWhereClause(q)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.tree, q.ConditionTree) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.tree, q.ConditionTree)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_Expect(t *testing.T) {
|
||||
type Case struct {
|
||||
param tokenizer.TokenType
|
||||
expected *tokenizer.Token
|
||||
}
|
||||
|
||||
input := "SELECT all FROM . WHERE name = foo OR size <> 100"
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(input)}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
param: tokenizer.Select,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Select, Raw: "SELECT"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.From,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Identifier, Raw: "all"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
param: tokenizer.From,
|
||||
expected: &tokenizer.Token{Type: tokenizer.From, Raw: "FROM"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Identifier, Raw: "."},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
param: tokenizer.Where,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Where, Raw: "WHERE"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Identifier, Raw: "name"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Equals,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Equals, Raw: "="},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Identifier, Raw: "foo"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Or,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Or, Raw: "OR"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Identifier, Raw: "size"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
param: tokenizer.NotEquals,
|
||||
expected: &tokenizer.Token{Type: tokenizer.NotEquals, Raw: "<>"},
|
||||
},
|
||||
{
|
||||
param: tokenizer.Identifier,
|
||||
expected: &tokenizer.Token{Type: tokenizer.Identifier, Raw: "100"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := p.expect(c.param)
|
||||
if !reflect.DeepEqual(c.expected, actual) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_SelectAllVariations(t *testing.T) {
|
||||
expected := &query.Query{
|
||||
Attributes: allAttributes,
|
||||
Sources: map[string][]string{
|
||||
"include": {"."},
|
||||
"exclude": {},
|
||||
},
|
||||
ConditionTree: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
SourceAliases: map[string]string{},
|
||||
Modifiers: map[string][]query.Modifier{},
|
||||
}
|
||||
|
||||
cases := []string{
|
||||
"FROM . WHERE name LIKE foo",
|
||||
"all FROM . WHERE name LIKE foo",
|
||||
"SELECT FROM . WHERE name LIKE foo",
|
||||
"SELECT all FROM . WHERE name LIKE foo",
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual, err := Run(c)
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_Run(t *testing.T) {
|
||||
type Expected struct {
|
||||
q *query.Query
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
// TODO: Add more cases.
|
||||
cases := []Case{
|
||||
{
|
||||
input: "SELECT all FROM . WHERE name LIKE foo",
|
||||
expected: Expected{
|
||||
q: &query.Query{
|
||||
Attributes: allAttributes,
|
||||
Sources: map[string][]string{
|
||||
"include": {"."},
|
||||
"exclude": {},
|
||||
},
|
||||
ConditionTree: &query.ConditionNode{
|
||||
Condition: &query.Condition{
|
||||
Attribute: "name",
|
||||
Operator: tokenizer.Like,
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
SourceAliases: map[string]string{},
|
||||
Modifiers: map[string][]query.Modifier{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual, err := Run(c.input)
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.q, actual) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.q, actual)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
// parseSourceList parses the list of directories passed to the FROM clause. If
|
||||
// a source is followed by the AS keyword, the following word is registered as
|
||||
// an alias.
|
||||
func (p *parser) parseSourceList(sources *map[string][]string,
|
||||
aliases *map[string]string) error {
|
||||
for {
|
||||
// If the next token is a hypen, exclude this directory.
|
||||
sourceType := "include"
|
||||
if token := p.expect(tokenizer.Hyphen); token != nil {
|
||||
sourceType = "exclude"
|
||||
}
|
||||
|
||||
source := p.expect(tokenizer.Identifier)
|
||||
if source == nil {
|
||||
return p.currentError()
|
||||
}
|
||||
source.Raw = filepath.Clean(source.Raw)
|
||||
(*sources)[sourceType] = append((*sources)[sourceType], source.Raw)
|
||||
|
||||
if token := p.expect(tokenizer.As); token != nil {
|
||||
alias := p.expect(tokenizer.Identifier)
|
||||
if alias == nil {
|
||||
return p.currentError()
|
||||
}
|
||||
if sourceType == "exclude" {
|
||||
return fmt.Errorf("cannot alias excluded directory %s", source.Raw)
|
||||
}
|
||||
(*aliases)[alias.Raw] = source.Raw
|
||||
}
|
||||
|
||||
if p.expect(tokenizer.Comma) == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kashav/fsql/tokenizer"
|
||||
)
|
||||
|
||||
func TestSourceParser_ExpectCorrectSources(t *testing.T) {
|
||||
type Expected struct {
|
||||
sources map[string][]string
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: ".",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{"include": {"."}},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "., ~/foo",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{"include": {".", "~/foo"}},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "., -.bar",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{
|
||||
"include": {"."},
|
||||
"exclude": {".bar"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "-.bar, ., ~/foo AS foo",
|
||||
expected: Expected{
|
||||
sources: map[string][]string{
|
||||
"include": {".", "~/foo"},
|
||||
"exclude": {".bar"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{input: "", expected: Expected{err: io.ErrUnexpectedEOF}},
|
||||
{input: "foo,", expected: Expected{err: io.ErrUnexpectedEOF}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
sources := make(map[string][]string, 0)
|
||||
aliases := make(map[string]string, 0)
|
||||
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(c.input)}
|
||||
err := p.parseSourceList(&sources, &aliases)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.sources, sources) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.sources, sources)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceParser_ExpectCorrectAliases(t *testing.T) {
|
||||
type Expected struct {
|
||||
aliases map[string]string
|
||||
err error
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
input string
|
||||
expected Expected
|
||||
}
|
||||
|
||||
cases := []Case{
|
||||
{
|
||||
input: ".",
|
||||
expected: Expected{
|
||||
aliases: map[string]string{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: ". AS cwd",
|
||||
expected: Expected{
|
||||
aliases: map[string]string{"cwd": "."},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "., -.bar, ~/foo AS foo",
|
||||
expected: Expected{
|
||||
aliases: map[string]string{"foo": "~/foo"},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "-.bar AS bar",
|
||||
expected: Expected{err: errors.New("cannot alias excluded directory .bar")},
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
{
|
||||
input: "foo AS",
|
||||
expected: Expected{err: io.ErrUnexpectedEOF},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
sources := make(map[string][]string, 0)
|
||||
aliases := make(map[string]string, 0)
|
||||
|
||||
p := &parser{tokenizer: tokenizer.NewTokenizer(c.input)}
|
||||
err := p.parseSourceList(&sources, &aliases)
|
||||
|
||||
if c.expected.err == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("\nExpected no error\n Got %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected.aliases, aliases) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.aliases, aliases)
|
||||
}
|
||||
} else if !reflect.DeepEqual(c.expected.err, err) {
|
||||
t.Fatalf("\nExpected %v\n Got %v", c.expected.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user