chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:14 +08:00
commit c581336356
61 changed files with 5678 additions and 0 deletions
+102
View File
@@ -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
}
}
}
+256
View File
@@ -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)
}
}
}
+210
View File
@@ -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
}
+408
View File
@@ -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)
}
}
}
+42
View File
@@ -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}
}
+28
View File
@@ -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)
}
}
+140
View File
@@ -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
}
+437
View File
@@ -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)
}
}
}
+45
View File
@@ -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
}
+149
View File
@@ -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)
}
}
}