3
0

Refactor pkg/ql for more flexibility

Created a temporary pkg/qlng package which contains the reworked
logic to avoid (potentially) breaking the rest of the system.

Most of the testing will be done implicitly via reporting tests.
This commit is contained in:
Tomaž Jerman
2021-08-05 09:31:04 +02:00
parent 9e5ae5df30
commit 25a9be1a54
9 changed files with 1193 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
package qlng
type (
ASTNode struct {
pMeta *parserMeta
Ref string `json:"ref,omitempty"`
Args ASTNodeSet `json:"args,omitempty"`
Symbol string `json:"symbol,omitempty"`
Value *typedValueWrap `json:"value,omitempty"`
Raw string `json:"raw,omitempty"`
}
ASTNodeSet []*ASTNode
typedValueWrap struct {
Value interface{} `json:"@value"`
Type string `json:"@type"`
}
parserMeta struct {
opDef *opDef
}
)
func MakeValueOf(t string, v interface{}) *typedValueWrap {
return &typedValueWrap{
Type: t,
Value: v,
}
}
// Traverse traverses the AST down to leaf nodes.
//
// If fnc. returns false, the traversal of the current branch ends.
func (n *ASTNode) Traverse(f func(*ASTNode) (bool, *ASTNode, error)) (err error) {
var ok bool
var r *ASTNode
if n == nil {
return nil
}
ok, r, err = f(n)
if err != nil {
return err
}
*n = *r
if !ok {
return
}
for _, a := range n.Args {
if err = a.Traverse(f); err != nil {
return
}
}
return
}
func (n ASTNode) Clone() *ASTNode {
aa := n.Args
if n.Args != nil {
n.Args = make(ASTNodeSet, len(aa))
for i, a := range aa {
n.Args[i] = a.Clone()
}
}
if n.Value != nil {
n.Value = &typedValueWrap{
Type: n.Value.Type,
Value: n.Value.Value,
}
}
return &n
}
func (n lNull) ToAST() (out *ASTNode) {
return &ASTNode{
Ref: "null",
}
}
func (n lBoolean) ToAST() (out *ASTNode) {
return &ASTNode{
Value: MakeValueOf("Boolean", n.value),
}
}
func (n lString) ToAST() (out *ASTNode) {
return &ASTNode{
Value: MakeValueOf("String", n.value),
}
}
// @todo differentiate between floats and others
func (n lNumber) ToAST() (out *ASTNode) {
if isFloaty(n.value) {
return &ASTNode{
Value: MakeValueOf("Float", n.value),
}
} else {
return &ASTNode{
Value: MakeValueOf("Int", n.value),
}
}
}
func (n operator) ToAST() (out *ASTNode) {
op := getOp(n.kind)
return &ASTNode{
Ref: op.name,
Args: make(ASTNodeSet, 0, 2),
pMeta: &parserMeta{opDef: op},
}
}
func (n Ident) ToAST() (out *ASTNode) {
return &ASTNode{
Symbol: n.Value,
}
}
func (n keyword) ToAST() (out *ASTNode) {
return &ASTNode{
Ref: n.keyword,
}
}
func (n interval) ToAST() (out *ASTNode) {
return &ASTNode{
Ref: "interval",
Args: ASTNodeSet{
{Symbol: n.unit},
{Value: MakeValueOf("Number", n.value)},
},
}
}
func (n function) ToAST() (out *ASTNode) {
auxA := n.arguments.ToAST()
return &ASTNode{
Ref: n.name,
Args: auxA.Args,
}
}
func (nn parserNodeSet) ToAST() (out *ASTNode) {
auxArgs := make(ASTNodeSet, 0, len(nn))
for _, n := range nn {
auxArgs = append(auxArgs, n.ToAST())
}
return &ASTNode{
Ref: "group",
Args: auxArgs,
}
}
func (nn parserNodes) ToAST() (out *ASTNode) {
// Prep
auxArgs := make(ASTNodeSet, 0, len(nn))
// Convert the entire level to AST nodes
for _, n := range nn {
auxArgs = append(auxArgs, n.ToAST())
}
// In the current level, have the operators consume operands
// based on their defined weight
//
// - find the highest prio. op and have it consume what it wants
// - repeat until all ops are satisfied
// -- post optimizations?
for {
var bestOp *opDef
bestOpIx := -1
// We're done when it is reduced to 1
if len(auxArgs) <= 1 {
break
}
for _i, _a := range auxArgs {
i := _i
a := _a
// use this as a delimiter for now
if a.pMeta == nil || a.pMeta.opDef == nil {
continue
}
if bestOp == nil {
bestOp = a.pMeta.opDef
bestOpIx = i
continue
}
if a.pMeta.opDef.weight < bestOp.weight {
bestOp = a.pMeta.opDef
bestOpIx = i
continue
}
}
// Have the op consume what it needs.
// Currently we only have binary operators (the !unary ones) so this is fine.
arg := auxArgs[bestOpIx]
arg.Args = append(arg.Args, auxArgs[bestOpIx-1], auxArgs[bestOpIx+1])
// this is not needed anymore so we can remove it
arg.pMeta = nil
// Remove the consumed bits and replace it with the new bit
aux := auxArgs[0 : bestOpIx-1]
aux = append(aux, arg)
// +1 for right side, +1 because the left index is inclusive
aux = append(aux, auxArgs[bestOpIx+2:]...)
auxArgs = aux
}
return &ASTNode{
Ref: "group",
Args: auxArgs,
}
}
+32
View File
@@ -0,0 +1,32 @@
package qlng
import (
"testing"
)
// Ensure the parser can parse strings into Statement ASTs.
func Test_Validators(t *testing.T) {
var tests = []struct {
tree parserNode
}{
{
tree: parserNodes{
Ident{Value: "foo"},
operator{kind: "="},
},
},
{
tree: parserNodes{
operator{kind: "="},
Ident{Value: "foo"},
},
},
}
for i, test := range tests {
if err := test.tree.Validate(); err == nil {
t.Fatalf("expecting error, got nil:\n"+
" test case: %d. %q", i, test.tree.String())
}
}
}
+112
View File
@@ -0,0 +1,112 @@
package qlng
import (
"bufio"
"io"
)
// Lexer represents a lexical scanner.
type (
RuneReader interface {
read() rune
unread()
}
TokenConsumers interface {
Test(ch rune) bool
Consume(s RuneReader) Token
}
Token struct {
code tokenCode
literal string
line uint
char uint
}
Lexer struct {
r *bufio.Reader
consumers []TokenConsumers
line uint
char uint
}
)
// eof represents a marker rune for the end of the reader.
var eof = rune(0)
// NewLexer returns a new instance of Lexer.
func NewLexer(r io.Reader) *Lexer {
return &Lexer{
r: bufio.NewReader(r),
consumers: []TokenConsumers{
&TokenConsumerGeneric{token: WS, whitelist: CHAR_WHITELIST_WHITESPACE},
// @todo ensure operator order (eg != is valid, =! is not)
&TokenConsumerGeneric{token: OPERATOR, whitelist: CHAR_WHITELIST_OPERATORS},
&TokenConsumerGeneric{token: COMMA, whitelist: ",", maxLength: 1},
&TokenConsumerGeneric{token: DOT, whitelist: ".", maxLength: 1},
&TokenConsumerGeneric{token: PARENTHESIS_OPEN, whitelist: "(", maxLength: 1},
&TokenConsumerGeneric{token: PARENTHESIS_CLOSE, whitelist: ")", maxLength: 1},
&TokenConsumerString{},
&TokenConsumerNumber{},
&TokenConsumerIdent{},
},
}
}
// Scan returns the next token and literal value.
func (s *Lexer) Scan() Token {
var ch = s.peek()
if ch == '\n' {
s.line++
s.char = 0
}
if eof == ch {
return Token{code: EOF, line: s.line, char: s.char}
}
for _, c := range s.consumers {
if c.Test(ch) {
t := c.Consume(s)
t.line = s.line
t.char = s.char
return t
}
}
return Token{code: ILLEGAL, literal: string(ch), line: s.line, char: s.char}
}
// read reads the next rune from the buffered reader.
// Returns the rune(0) if an error occurs (or io.EOF is returned).
func (s *Lexer) read() rune {
ch, _, err := s.r.ReadRune()
s.char++
if err != nil {
return eof
}
return ch
}
func (s *Lexer) peek() rune {
bb, err := s.r.Peek(1)
if err != nil || len(bb) == 0 {
return eof
}
return rune(bb[0])
}
// unread places the previously read rune back on the reader.
func (s *Lexer) unread() { _ = s.r.UnreadRune() }
func (t Token) Is(cc ...tokenCode) bool {
for _, c := range cc {
if t.code == c {
return true
}
}
return false
}
+105
View File
@@ -0,0 +1,105 @@
package qlng
import (
"strings"
"testing"
)
// Ensure the scanner can scan tokens correctly.
func TestScanner_ScanSimple(t *testing.T) {
var tests = []struct {
s string
tok tokenCode
lit string
}{
// Special tokens (EOF, ILLEGAL, WS)
{s: ``, tok: EOF},
{s: `#`, tok: ILLEGAL, lit: `#`},
{s: ` `, tok: WS, lit: " "},
{s: "\t", tok: WS, lit: "\t"},
{s: "\n", tok: WS, lit: "\n"},
// Operators
{s: `*`, tok: OPERATOR, lit: "*"},
{s: `!=`, tok: OPERATOR, lit: "!="},
{s: `<`, tok: OPERATOR, lit: "<"},
{s: `>`, tok: OPERATOR, lit: ">"},
{s: `>=`, tok: OPERATOR, lit: ">="},
{s: `<>`, tok: OPERATOR, lit: "<>"},
{s: `+`, tok: OPERATOR, lit: "+"},
{s: `'fooo'`, tok: LSTRING, lit: "fooo"},
{s: `'escaped \' quote'`, tok: LSTRING, lit: "escaped ' quote"},
{s: `'double \\ escape'`, tok: LSTRING, lit: "double \\ escape"},
{s: `12345`, tok: LNUMBER, lit: "12345"},
// Identifiers
{s: `foo`, tok: IDENT, lit: `foo`},
{s: `Zx12_3U_-`, tok: IDENT, lit: `Zx12_3U_`},
// Parenthesis
{s: `(`, tok: PARENTHESIS_OPEN, lit: `(`},
{s: `)`, tok: PARENTHESIS_CLOSE, lit: `)`},
// Literals
{s: `true`, tok: LBOOL, lit: `TRUE`},
}
for i, test := range tests {
s := NewLexer(strings.NewReader(test.s))
tok := s.Scan()
if test.tok != tok.code {
t.Errorf("%d. %q token mismatch: exp=%d got=%d <%q>", i, test.s, test.tok, tok.code, tok.literal)
} else if test.lit != tok.literal {
t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, test.s, test.lit, tok.literal)
}
}
}
func TestScanner_ScanComplex(t *testing.T) {
var tests = []struct {
s string
tokens []tokenCode
}{
// Special tokens (EOF, ILLEGAL, WS)
{`func(arg1, arg2)`,
[]tokenCode{IDENT, PARENTHESIS_OPEN, IDENT, COMMA, WS, IDENT, PARENTHESIS_CLOSE}},
{`arg1 * arg2`,
[]tokenCode{IDENT, WS, OPERATOR, WS, IDENT}},
{`date_format(created_at,'%Y')`,
[]tokenCode{IDENT, PARENTHESIS_OPEN, IDENT, COMMA, LSTRING, PARENTHESIS_CLOSE}},
{`foo LIKE 'abc%'`,
[]tokenCode{IDENT, WS, OPERATOR, WS, LSTRING}},
{`foo NOT LIKE 'abc%'`,
[]tokenCode{IDENT, WS, OPERATOR, WS, OPERATOR, WS, LSTRING}},
{`foo DESC`,
[]tokenCode{IDENT, WS, KEYWORD}},
{`year(now())-1`,
[]tokenCode{IDENT, PARENTHESIS_OPEN, IDENT, PARENTHESIS_OPEN, PARENTHESIS_CLOSE, PARENTHESIS_CLOSE, OPERATOR, LNUMBER}},
}
for _, test := range tests {
var tokens []Token
s := NewLexer(strings.NewReader(test.s))
for {
tok := s.Scan()
if tok.Is(EOF) {
break
}
tokens = append(tokens, tok)
}
if len(tokens) != len(test.tokens) {
t.Errorf("Collected tokens do not match (%v)", tokens)
}
for i := 0; i < len(tokens); i++ {
if tokens[i].code != test.tokens[i] {
t.Errorf("Input: %s", test.s)
t.Errorf("Expected: %v", test.tokens)
t.Errorf("Collected: %v", tokens)
break
}
}
}
}
+301
View File
@@ -0,0 +1,301 @@
package qlng
import (
"fmt"
"strings"
)
type (
// Parser represents a parser.
Parser struct {
lexer *Lexer
tokbuf []Token
OnIdent IdentHandler
OnFunction FunctionHandler
// parenthesis level control
level uint
}
IdentHandler func(ident Ident) (Ident, error)
FunctionHandler func(ident function) (parserNode, error)
)
// NewParser returns a new instance of Parser.
func NewParser() *Parser {
p := &Parser{
OnIdent: func(ident Ident) (Ident, error) { return ident, nil },
OnFunction: func(ident function) (parserNode, error) { return ident, nil },
}
return p
}
// Removes oldest token in the buffer, adds new one and returns 2nd oldest
func (p *Parser) nextToken() Token {
var t Token
for {
t = p.lexer.Scan()
if !t.Is(WS) {
p.tokbuf = append(p.tokbuf[1:], t)
return p.tokbuf[0]
}
}
}
func (p *Parser) peekToken(s int) Token {
return p.tokbuf[s]
}
func (p *Parser) initLexer(s string) {
p.lexer = NewLexer(strings.NewReader(s))
p.tokbuf = make([]Token, 3)
for c := 1; c < cap(p.tokbuf); c++ {
// Fill the buffer
p.nextToken()
}
}
// Parse parses the given expression and returns the generated AST
func (p *Parser) Parse(s string) (*ASTNode, error) {
p.initLexer(s)
if set, err := p.parse(p.nextToken()); err != nil {
return nil, err
} else if len(set) == 1 {
return set[0].ToAST(), set[0].Validate()
} else {
return set.ToAST(), set.Validate()
}
}
// Peek ahead if there is an alias ident (<IDENT:AS>)
//
// @todo remove this; not used anywhere after report builder gets removed
func (p *Parser) peekIfAlias() bool {
var f, s = p.peekToken(1), p.peekToken(2)
return f.Is(IDENT) && strings.ToUpper(f.literal) == "AS" && s.Is(IDENT)
}
func (p *Parser) parse(t Token) (list parserNodes, err error) {
goto checkToken
next:
if p.peekToken(1).Is(COMMA) || p.peekIfAlias() {
// Peek ahead and exit on comma
return
}
if p.peekToken(1).Is(PARENTHESIS_CLOSE) {
// Peek ahead and exit on closed parenthesis
if p.level == 0 {
return nil, fmt.Errorf("closing unopened parenthesis in expression")
}
p.level--
return
}
t = p.nextToken()
checkToken:
switch t.code {
case EOF:
break
case WS:
// Ignore ws... next token...
goto next
case ILLEGAL:
return nil, fmt.Errorf("found an illegal token (%+v)", t)
case IDENT:
var ident parserNode
if ident, err = p.parseIdent(t); err != nil {
return nil, err
} else {
list = append(list, ident)
goto next
}
case LNULL:
list = append(list, lNull{})
goto next
case LBOOL:
list = append(list, lBoolean{value: evalBool(t.literal)})
goto next
case OPERATOR:
if len(list) > 0 {
// Merge with previous operator node
if prevOp, ok := list[len(list)-1].(operator); ok {
list[len(list)-1] = operator{kind: prevOp.kind + " " + t.literal}
goto next
}
}
list = append(list, operator{kind: t.literal})
goto next
case KEYWORD:
if keyword, err := p.parseKeyword(t); err != nil {
return nil, err
} else {
list = append(list, keyword)
}
goto next
case LNUMBER:
list = append(list, lNumber{value: t.literal})
goto next
case LSTRING:
list = append(list, lString{value: t.literal})
goto next
case PARENTHESIS_OPEN:
depth := p.level
p.level++
if sub, err := p.parse(p.nextToken()); err != nil {
return nil, err
} else {
list = append(list, sub)
}
// Allow parent level to continue parsing.
// Example: ((A) AND (B))
// +1 since PARENTHESIS_CLOSE decrease level
if (p.level + 1) != depth {
p.nextToken()
goto next
}
default:
return nil, fmt.Errorf("unexpected token while parsing expression (%v)", t)
}
return list, nil
}
func (p *Parser) parseIdent(t Token) (list parserNode, err error) {
if p.peekToken(1).Is(PARENTHESIS_OPEN) {
// Handle function calls: <IDENT><PARENTHESIS_OPEN>...
f := function{name: t.literal}
if f.arguments, err = p.parseSet(); err != nil {
return nil, err
} else {
return p.OnFunction(f)
}
}
i := Ident{Value: t.literal}
if p.peekToken(1).Is(DOT) {
p.nextToken()
i.Value += "."
l2 := p.nextToken()
if l2.Is(IDENT) {
i.Value += l2.literal
}
}
return p.OnIdent(i)
}
func (p *Parser) parseSet() (list parserNodeSet, err error) {
var expr parserNodes
var parenthesisOpened = false
next:
t := p.nextToken()
if p.peekIfAlias() {
return
}
switch t.code {
case WS:
goto next
case PARENTHESIS_OPEN:
p.level++
parenthesisOpened = true
goto next
case LNUMBER:
list = append(list, lNumber{value: t.literal})
goto next
case LSTRING:
list = append(list, lString{value: t.literal})
goto next
case OPERATOR:
list = append(list, operator{kind: t.literal})
goto next
case KEYWORD:
if keyword, err := p.parseKeyword(t); err != nil {
return nil, err
} else {
list = append(list, keyword)
}
goto next
case IDENT:
if p.peekToken(1).Is(OPERATOR) {
// Looks like we have an expression ahead of us
if parenthesisOpened {
// Expression will find closing parenthesis and dec. the level
// so, lets bump up the number
p.level++
}
if expr, err = p.parse(t); err != nil {
return
}
list = append(list, expr)
goto next
}
var ident parserNode
if ident, err = p.parseIdent(t); err != nil {
return nil, err
} else {
list = append(list, ident)
goto next
}
case COMMA:
goto next
case EOF:
return
case PARENTHESIS_CLOSE:
// Peek ahead and exit on closed parenthesis
if p.level == 0 {
return nil, fmt.Errorf("closing unopened parenthesis in set")
}
p.level--
return
default:
return nil, fmt.Errorf("unexpected token while parsing set (%v)", t)
}
}
func (p *Parser) parseKeyword(t Token) (list parserNode, err error) {
switch strings.ToUpper(t.literal) {
case "INTERVAL":
i := interval{value: p.nextToken().literal}
u := p.nextToken()
if u.code != IDENT {
return nil, fmt.Errorf("expecting identifier, got %v", t)
} else {
switch strings.ToUpper(u.literal) {
case "MICROSECOND", "SECOND", "MINUTE", "HOUR",
"DAY", "WEEK", "MONTH", "QUARTER", "YEAR",
"SECOND_MICROSECOND", "MINUTE_MICROSECOND", "MINUTE_SECOND", "HOUR_MICROSECOND", "HOUR_SECOND",
"HOUR_MINUTE", "DAY_MICROSECOND", "DAY_SECOND", "DAY_MINUTE", "DAY_HOUR", "YEAR_MONTH":
// All good
break
default:
return nil, fmt.Errorf("expecting interval unit, got %v", u.literal)
}
}
i.unit = u.literal
return i, nil
default:
return keyword{keyword: t.literal}, nil
}
}
+204
View File
@@ -0,0 +1,204 @@
package qlng
import (
"fmt"
"strings"
)
// SelectStatement represents a SQL SELECT statement.
type (
parserNode interface {
fmt.Stringer
Validate() error
ToAST() *ASTNode
}
parserNodeSet []parserNode // Stream of comma delimited nodes
parserNodes []parserNode // Stream of space delimited nodes
lNull struct{}
lBoolean struct{ value bool }
lString struct {
value string
args []interface{}
}
lNumber struct {
value string
}
operator struct {
kind string
}
Ident struct {
Value string
args []interface{}
}
keyword struct {
keyword string
}
interval struct {
value string
unit string
}
function struct {
name string
arguments parserNodeSet
}
opDef struct {
name string
weight int
}
)
var (
ops = map[string]opDef{
// generic comparison
`=`: {name: `eq`, weight: 40},
`==`: {name: `eq`, weight: 40},
`===`: {name: `eq`, weight: 40},
`!=`: {name: `ne`, weight: 40},
`!==`: {name: `ne`, weight: 40},
`<>`: {name: `ne`, weight: 40},
`<`: {name: `lt`, weight: 30},
`<=`: {name: `le`, weight: 30},
`>`: {name: `gt`, weight: 30},
`>=`: {name: `ge`, weight: 30},
`IS`: {name: `is`, weight: 40},
`IS NOT`: {name: `nis`, weight: 40},
// conjunction
`AND`: {name: `and`, weight: 50},
`&&`: {name: `and`, weight: 50},
`OR`: {name: `or`, weight: 60},
`||`: {name: `or`, weight: 60},
`XOR`: {name: `xor`, weight: 60},
// math
`+`: {name: `add`, weight: 20},
`-`: {name: `sub`, weight: 20},
`*`: {name: `mult`, weight: 10},
`/`: {name: `div`, weight: 10},
// str comp.
`LIKE`: {name: `like`, weight: 40},
`NOT LIKE`: {name: `nlike`, weight: 40},
}
)
func getOp(op string) *opDef {
o, ok := ops[strings.ToUpper(op)]
if !ok {
return nil
}
return &o
}
func (n lNull) Validate() (err error) { return }
func (n lNull) String() string { return "NULL" }
func (n lBoolean) Validate() (err error) { return }
func (n lBoolean) String() string {
if n.value {
return "TRUE"
} else {
return "FALSE"
}
}
func (n lString) Validate() (err error) { return }
func (n lString) String() string { return fmt.Sprintf("%q", n.value) }
func (n lNumber) Validate() (err error) { return }
func (n lNumber) String() string { return n.value }
func (n operator) Validate() (err error) {
if getOp(n.kind) == nil {
return fmt.Errorf("unknown operator '%s'", n.kind)
}
return
}
func (n operator) String() string { return n.kind }
func (n keyword) Validate() (err error) { return }
func (n keyword) String() string { return n.keyword }
func (n interval) Validate() (err error) { return }
func (n interval) String() string { return fmt.Sprintf("INTERVAL %s %s", n.value, n.unit) }
func (n function) Validate() (err error) { return }
func (n function) String() string { return fmt.Sprintf("%s(%s)", n.name, n.arguments) }
func (n Ident) Validate() (err error) { return }
func (n Ident) String() string { return n.Value }
func (nn parserNodes) Validate() (err error) {
if err = validate(nn); err != nil {
return
}
l := len(nn)
if l == 0 {
return fmt.Errorf("empty set")
}
if op, ok := nn[0].(operator); ok {
return fmt.Errorf("malformed expression, unexpected operator '%s' at first node", op)
}
if l > 1 {
if op, ok := nn[l-1].(operator); ok {
return fmt.Errorf("malformed expression, unexpected operator '%s' at last node", op)
}
}
return
}
func (nn parserNodes) String() (out string) {
for i, n := range nn {
if i > 0 {
out = out + " "
}
out = out + n.String()
}
return
}
func (nn parserNodeSet) Validate() (err error) {
return validate(nn)
}
func (nn parserNodeSet) String() (out string) {
for i, n := range nn {
if i > 0 {
out = out + ", "
}
out = out + n.String()
}
return
}
func validate(nn []parserNode) (err error) {
if len(nn) == 0 {
return fmt.Errorf("empty node set")
}
for _, n := range nn {
if err = n.Validate(); err != nil {
return
}
}
return
}
+29
View File
@@ -0,0 +1,29 @@
package qlng
type (
tokenCode int
)
const (
CHAR_WHITELIST_WHITESPACE = " \n\t"
CHAR_WHITELIST_OPERATORS = "!+-/*=<>&|"
CHAR_WHITELIST_QUOTES = "'"
)
const (
// Special tokens
ILLEGAL tokenCode = iota
EOF
WS // 2
IDENT
LNULL
LBOOL // 4
LNUMBER
LSTRING
COMMA // ,
DOT // .
OPERATOR // + - / *
PARENTHESIS_OPEN
PARENTHESIS_CLOSE
KEYWORD
)
+159
View File
@@ -0,0 +1,159 @@
package qlng
import (
"bytes"
"strings"
)
type (
TokenConsumerWS struct{}
TokenConsumerIdent struct{}
TokenConsumerOperator struct{}
TokenConsumerComma struct{}
TokenConsumerString struct{}
TokenConsumerNumber struct{}
TokenConsumerGeneric struct {
token tokenCode
whitelist string
maxLength int
}
)
func in(ch rune, wl string) bool {
var w rune
for _, w = range wl {
if ch == w {
return true
}
}
return false
}
func (g TokenConsumerGeneric) Test(ch rune) bool {
return in(ch, g.whitelist)
}
func (g TokenConsumerGeneric) Consume(s RuneReader) Token {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent whitespace character into the buffer.
// Non-whitespace characters and EOF will cause the loop to exit.
for {
if g.maxLength > 0 && buf.Len() >= g.maxLength {
// Length control
break
}
if ch := s.read(); ch == eof {
break
} else if !g.Test(ch) {
s.unread()
break
} else {
buf.WriteRune(ch)
}
}
return Token{code: g.token, literal: buf.String()}
}
func (i TokenConsumerIdent) Test(ch rune) bool {
return isLetter(ch)
}
// Consumes the current rune and all contiguous ident runes.
func (TokenConsumerIdent) Consume(s RuneReader) Token {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
// Read every subsequent ident character into the buffer.
// Non-ident characters and EOF will cause the loop to exit.
for {
if ch := s.read(); ch == eof {
break
} else if !isLetter(ch) && !isDigit(ch) && ch != '_' {
s.unread()
break
} else {
_, _ = buf.WriteRune(ch)
}
}
lit := strings.ToUpper(buf.String())
switch lit {
case "NULL":
return Token{code: LNULL}
case "TRUE", "FALSE":
return Token{code: LBOOL, literal: lit}
case "IS", "LIKE", "NOT", "AND", "OR", "XOR":
return Token{code: OPERATOR, literal: lit}
case "DESC", "ASC", "INTERVAL":
return Token{code: KEYWORD, literal: lit}
}
// Otherwise return as a regular identifier.
return Token{code: IDENT, literal: buf.String()}
}
func (str TokenConsumerString) Test(ch rune) bool {
return in(ch, CHAR_WHITELIST_QUOTES)
}
// Consumes entire string (skipping quotes)
func (str TokenConsumerString) Consume(s RuneReader) Token {
var buf bytes.Buffer
var escaping = false
var ch = s.read() // skip quite
for {
if ch = s.read(); ch == eof {
break
} else if !escaping && str.Test(ch) { // test for quote
return Token{code: LSTRING, literal: buf.String()}
} else {
escaping = !escaping && ch == '\\'
if !escaping {
// Add char to buffer if not escaping
_, _ = buf.WriteRune(ch)
}
}
}
// This string did not end properly (with an enclosing quote).
return Token{code: ILLEGAL, literal: buf.String() + string(ch)}
}
func (str TokenConsumerNumber) Test(ch rune) bool {
return isDigit(ch)
}
// Consumes entire number (very naive and simplified)
func (str TokenConsumerNumber) Consume(s RuneReader) Token {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(s.read())
for {
if ch := s.read(); ch == eof {
break
} else if !isDigit(ch) {
s.unread()
break
} else {
_, _ = buf.WriteRune(ch)
}
}
// Otherwise return as a regular identifier.
return Token{code: LNUMBER, literal: buf.String()}
}
// isLetter returns true if the rune is a letter.
func isLetter(ch rune) bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') }
// isDigit returns true if the rune is a digit.
func isDigit(ch rune) bool { return ch >= '0' && ch <= '9' }
+20
View File
@@ -0,0 +1,20 @@
package qlng
import (
"regexp"
"strings"
)
var (
truthy = regexp.MustCompile(`^(t(rue)?|y(es)?|1)$`)
)
// Check what boolean value the given string conforms to
func evalBool(v string) bool {
return truthy.MatchString(strings.ToLower(v))
}
// Check if the given string is a float
func isFloaty(v string) bool {
return strings.Contains(v, ".")
}