Refactor Super Graph into a library #26
This commit is contained in:
7
core/internal/qcode/bench.0
Normal file
7
core/internal/qcode/bench.0
Normal file
@ -0,0 +1,7 @@
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/dosco/super-graph/qcode
|
||||
BenchmarkQCompile-8 100000 18528 ns/op 9208 B/op 107 allocs/op
|
||||
BenchmarkQCompileP-8 300000 5952 ns/op 9208 B/op 107 allocs/op
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/qcode 3.893s
|
7
core/internal/qcode/bench.1
Normal file
7
core/internal/qcode/bench.1
Normal file
@ -0,0 +1,7 @@
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/dosco/super-graph/qcode
|
||||
BenchmarkQCompile-8 100000 18240 ns/op 7454 B/op 88 allocs/op
|
||||
BenchmarkQCompileP-8 300000 5788 ns/op 7494 B/op 88 allocs/op
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/qcode 3.813s
|
7
core/internal/qcode/bench.2
Normal file
7
core/internal/qcode/bench.2
Normal file
@ -0,0 +1,7 @@
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/dosco/super-graph/qcode
|
||||
BenchmarkQCompile-8 100000 17231 ns/op 3352 B/op 87 allocs/op
|
||||
BenchmarkQCompileP-8 300000 5023 ns/op 3387 B/op 87 allocs/op
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/qcode 3.462s
|
7
core/internal/qcode/bench.3
Normal file
7
core/internal/qcode/bench.3
Normal file
@ -0,0 +1,7 @@
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/dosco/super-graph/qcode
|
||||
BenchmarkQCompile-8 200000 10029 ns/op 2291 B/op 38 allocs/op
|
||||
BenchmarkQCompileP-8 500000 2925 ns/op 2298 B/op 38 allocs/op
|
||||
PASS
|
||||
ok github.com/dosco/super-graph/qcode 3.616s
|
2
core/internal/qcode/cleanup.sh
Executable file
2
core/internal/qcode/cleanup.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
cd corpus && rm -rf $(find . ! -name '00?.gql')
|
136
core/internal/qcode/config.go
Normal file
136
core/internal/qcode/config.go
Normal file
@ -0,0 +1,136 @@
|
||||
package qcode
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Blocklist []string
|
||||
}
|
||||
|
||||
type QueryConfig struct {
|
||||
Limit int
|
||||
Filters []string
|
||||
Columns []string
|
||||
DisableFunctions bool
|
||||
}
|
||||
|
||||
type InsertConfig struct {
|
||||
Filters []string
|
||||
Columns []string
|
||||
Presets map[string]string
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Filters []string
|
||||
Columns []string
|
||||
Presets map[string]string
|
||||
}
|
||||
|
||||
type DeleteConfig struct {
|
||||
Filters []string
|
||||
Columns []string
|
||||
}
|
||||
|
||||
type TRConfig struct {
|
||||
Query QueryConfig
|
||||
Insert InsertConfig
|
||||
Update UpdateConfig
|
||||
Delete DeleteConfig
|
||||
}
|
||||
|
||||
type trval struct {
|
||||
query struct {
|
||||
limit string
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
disable struct {
|
||||
funcs bool
|
||||
}
|
||||
}
|
||||
|
||||
insert struct {
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
psmap map[string]string
|
||||
pslist []string
|
||||
}
|
||||
|
||||
update struct {
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
psmap map[string]string
|
||||
pslist []string
|
||||
}
|
||||
|
||||
delete struct {
|
||||
fil *Exp
|
||||
filNU bool
|
||||
cols map[string]struct{}
|
||||
}
|
||||
}
|
||||
|
||||
func (trv *trval) allowedColumns(qt QType) map[string]struct{} {
|
||||
switch qt {
|
||||
case QTQuery:
|
||||
return trv.query.cols
|
||||
case QTInsert:
|
||||
return trv.insert.cols
|
||||
case QTUpdate:
|
||||
return trv.update.cols
|
||||
case QTDelete:
|
||||
return trv.delete.cols
|
||||
case QTUpsert:
|
||||
return trv.insert.cols
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (trv *trval) filter(qt QType) (*Exp, bool) {
|
||||
switch qt {
|
||||
case QTQuery:
|
||||
return trv.query.fil, trv.query.filNU
|
||||
case QTInsert:
|
||||
return trv.insert.fil, trv.insert.filNU
|
||||
case QTUpdate:
|
||||
return trv.update.fil, trv.update.filNU
|
||||
case QTDelete:
|
||||
return trv.delete.fil, trv.delete.filNU
|
||||
case QTUpsert:
|
||||
return trv.insert.fil, trv.insert.filNU
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func listToMap(list []string) map[string]struct{} {
|
||||
m := make(map[string]struct{}, len(list))
|
||||
for i := range list {
|
||||
m[strings.ToLower(list[i])] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func mapToList(m map[string]string) []string {
|
||||
list := []string{}
|
||||
for k := range m {
|
||||
list = append(list, strings.ToLower(k))
|
||||
}
|
||||
sort.Strings(list)
|
||||
return list
|
||||
}
|
||||
|
||||
var varRe = regexp.MustCompile(`\$([a-zA-Z0-9_]+)`)
|
||||
|
||||
func parsePresets(m map[string]string) map[string]string {
|
||||
for k, v := range m {
|
||||
m[k] = varRe.ReplaceAllString(v, `{{$1}}`)
|
||||
}
|
||||
return m
|
||||
}
|
21
core/internal/qcode/corpus/001.gql
Normal file
21
core/internal/qcode/corpus/001.gql
Normal file
@ -0,0 +1,21 @@
|
||||
query {
|
||||
products(
|
||||
# returns only 30 items
|
||||
limit: 30,
|
||||
|
||||
# starts from item 10, commented out for now
|
||||
# offset: 10,
|
||||
|
||||
# orders the response items by highest price
|
||||
order_by: { price: desc },
|
||||
|
||||
# no duplicate prices returned
|
||||
distinct: [ price ]
|
||||
|
||||
# only items with an id >= 30 and < 30 are returned
|
||||
where: { id: { and: { greater_or_equals: 20, lt: 28 } } }) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}
|
14
core/internal/qcode/corpus/002.gql
Normal file
14
core/internal/qcode/corpus/002.gql
Normal file
@ -0,0 +1,14 @@
|
||||
query {
|
||||
products(
|
||||
where: {
|
||||
or: {
|
||||
not: { id: { is_null: true } },
|
||||
price: { gt: 10 },
|
||||
price: { lt: 20 }
|
||||
} }
|
||||
) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}
|
12
core/internal/qcode/corpus/003.gql
Normal file
12
core/internal/qcode/corpus/003.gql
Normal file
@ -0,0 +1,12 @@
|
||||
query {
|
||||
products(
|
||||
where: {
|
||||
and: {
|
||||
not: { id: { is_null: true } },
|
||||
price: { gt: 10 }
|
||||
}}) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}
|
||||
}
|
20
core/internal/qcode/fuzz.go
Normal file
20
core/internal/qcode/fuzz.go
Normal file
@ -0,0 +1,20 @@
|
||||
// +build gofuzz
|
||||
|
||||
package qcode
|
||||
|
||||
// FuzzerEntrypoint for Fuzzbuzz
|
||||
func Fuzz(data []byte) int {
|
||||
qt := GetQType(string(data))
|
||||
|
||||
if qt > QTUpsert {
|
||||
panic("qt > QTUpsert")
|
||||
}
|
||||
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
_, err := qcompile.Compile(data, "user")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
495
core/internal/qcode/lex.go
Normal file
495
core/internal/qcode/lex.go
Normal file
@ -0,0 +1,495 @@
|
||||
package qcode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
queryToken = []byte("query")
|
||||
mutationToken = []byte("mutation")
|
||||
subscriptionToken = []byte("subscription")
|
||||
trueToken = []byte("true")
|
||||
falseToken = []byte("false")
|
||||
quotesToken = []byte(`'"`)
|
||||
signsToken = []byte(`+-`)
|
||||
punctuatorToken = []byte(`!():=[]{|}`)
|
||||
spreadToken = []byte(`...`)
|
||||
digitToken = []byte(`0123456789`)
|
||||
dotToken = []byte(`.`)
|
||||
)
|
||||
|
||||
// Pos represents a byte position in the original input text from which
|
||||
// this template was parsed.
|
||||
type Pos int
|
||||
|
||||
// item represents a token or text string returned from the scanner.
|
||||
type item struct {
|
||||
_type itemType // The type of this item.
|
||||
pos Pos // The starting position, in bytes, of this item in the input string.
|
||||
end Pos // The ending position, in bytes, of this item in the input string.
|
||||
line int16 // The line number at the start of this item.
|
||||
}
|
||||
|
||||
// itemType identifies the type of lex items.
|
||||
type itemType int
|
||||
|
||||
const (
|
||||
itemError itemType = iota // error occurred; value is text of error
|
||||
itemEOF
|
||||
itemName
|
||||
itemQuery
|
||||
itemMutation
|
||||
itemSub
|
||||
itemPunctuator
|
||||
itemArgsOpen
|
||||
itemArgsClose
|
||||
itemListOpen
|
||||
itemListClose
|
||||
itemObjOpen
|
||||
itemObjClose
|
||||
itemColon
|
||||
itemEquals
|
||||
itemDirective
|
||||
itemVariable
|
||||
itemSpread
|
||||
itemIntVal
|
||||
itemFloatVal
|
||||
itemStringVal
|
||||
itemBoolVal
|
||||
)
|
||||
|
||||
// !$():=@[]{|}
|
||||
var punctuators = map[rune]itemType{
|
||||
'{': itemObjOpen,
|
||||
'}': itemObjClose,
|
||||
'[': itemListOpen,
|
||||
']': itemListClose,
|
||||
'(': itemArgsOpen,
|
||||
')': itemArgsClose,
|
||||
':': itemColon,
|
||||
'=': itemEquals,
|
||||
}
|
||||
|
||||
const eof = -1
|
||||
|
||||
// stateFn represents the state of the scanner as a function that returns the next state.
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
// lexer holds the state of the scanner.
|
||||
type lexer struct {
|
||||
input []byte // the string being scanned
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
items []item // array of scanned items
|
||||
itemsA [50]item
|
||||
line int16 // 1+number of newlines seen
|
||||
err error
|
||||
}
|
||||
|
||||
var zeroLex = lexer{}
|
||||
|
||||
func (l *lexer) Reset() {
|
||||
*l = zeroLex
|
||||
}
|
||||
|
||||
// next returns the next byte in the input.
|
||||
func (l *lexer) next() rune {
|
||||
if int(l.pos) >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRune(l.input[l.pos:])
|
||||
l.width = Pos(w)
|
||||
l.pos += l.width
|
||||
if r == '\n' {
|
||||
l.line++
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next rune in the input.
|
||||
func (l *lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per call of next.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
// Correct newline count.
|
||||
if l.width == 1 && l.input[l.pos] == '\n' {
|
||||
l.line--
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lexer) current() (Pos, Pos) {
|
||||
return l.start, l.pos
|
||||
}
|
||||
|
||||
// emit passes an item back to the client.
|
||||
func (l *lexer) emit(t itemType) {
|
||||
l.items = append(l.items, item{t, l.start, l.pos, l.line})
|
||||
// Some items contain text internally. If so, count their newlines.
|
||||
switch t {
|
||||
case itemStringVal:
|
||||
for i := l.start; i < l.pos; i++ {
|
||||
if l.input[i] == '\n' {
|
||||
l.line++
|
||||
}
|
||||
}
|
||||
}
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *lexer) emitL(t itemType) {
|
||||
s, e := l.current()
|
||||
lowercase(l.input, s, e)
|
||||
l.emit(t)
|
||||
}
|
||||
|
||||
// ignore skips over the pending input before this point.
|
||||
func (l *lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// accept consumes the next rune if it's from the valid set.
|
||||
func (l *lexer) accept(valid []byte) bool {
|
||||
if bytes.ContainsRune(valid, l.next()) {
|
||||
return true
|
||||
}
|
||||
l.backup()
|
||||
return false
|
||||
}
|
||||
|
||||
// acceptAlphaNum consumes a run of runes while they are alpha nums
|
||||
func (l *lexer) acceptAlphaNum() bool {
|
||||
n := 0
|
||||
for r := l.next(); isAlphaNumeric(r); r = l.next() {
|
||||
n++
|
||||
}
|
||||
l.backup()
|
||||
return (n != 0)
|
||||
}
|
||||
|
||||
// acceptComment consumes a run of runes while till the end of line
|
||||
func (l *lexer) acceptComment() {
|
||||
n := 0
|
||||
for r := l.next(); !isEndOfLine(r); r = l.next() {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
// acceptRun consumes a run of runes from the valid set.
|
||||
func (l *lexer) acceptRun(valid []byte) {
|
||||
for bytes.ContainsRune(valid, l.next()) {
|
||||
}
|
||||
l.backup()
|
||||
}
|
||||
|
||||
// errorf returns an error token and terminates the scan by passing
|
||||
// back a nil pointer that will be the next state, terminating l.nextItem.
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
l.err = fmt.Errorf(format, args...)
|
||||
l.items = append(l.items, item{itemError, l.start, l.pos, l.line})
|
||||
return nil
|
||||
}
|
||||
|
||||
// lex creates a new scanner for the input string.
|
||||
func lex(l *lexer, input []byte) error {
|
||||
if len(input) == 0 {
|
||||
return errors.New("empty query")
|
||||
}
|
||||
|
||||
l.input = input
|
||||
l.items = l.itemsA[:0]
|
||||
l.line = 1
|
||||
|
||||
l.run()
|
||||
|
||||
if last := l.items[len(l.items)-1]; last._type == itemError {
|
||||
return l.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// run runs the state machine for the lexer.
|
||||
func (l *lexer) run() {
|
||||
for state := lexRoot; state != nil; {
|
||||
state = state(l)
|
||||
}
|
||||
}
|
||||
|
||||
// lexInsideAction scans the elements inside action delimiters.
|
||||
func lexRoot(l *lexer) stateFn {
|
||||
r := l.next()
|
||||
|
||||
switch {
|
||||
case r == eof:
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
case isEndOfLine(r):
|
||||
l.ignore()
|
||||
case isSpace(r):
|
||||
l.ignore()
|
||||
case r == '#':
|
||||
l.ignore()
|
||||
l.acceptComment()
|
||||
l.ignore()
|
||||
case r == '@':
|
||||
l.ignore()
|
||||
if l.acceptAlphaNum() {
|
||||
l.emit(itemDirective)
|
||||
}
|
||||
case r == '$':
|
||||
l.ignore()
|
||||
if l.acceptAlphaNum() {
|
||||
s, e := l.current()
|
||||
lowercase(l.input, s, e)
|
||||
l.emit(itemVariable)
|
||||
}
|
||||
case contains(l.input, l.start, l.pos, punctuatorToken):
|
||||
if item, ok := punctuators[r]; ok {
|
||||
l.emit(item)
|
||||
} else {
|
||||
l.emit(itemPunctuator)
|
||||
}
|
||||
case r == '"' || r == '\'':
|
||||
l.backup()
|
||||
return lexString
|
||||
case r == '.':
|
||||
if len(l.input) >= 3 {
|
||||
if equals(l.input, 0, 3, spreadToken) {
|
||||
l.emit(itemSpread)
|
||||
return lexRoot
|
||||
}
|
||||
}
|
||||
fallthrough // '.' can start a number.
|
||||
case r == '+' || r == '-' || ('0' <= r && r <= '9'):
|
||||
l.backup()
|
||||
return lexNumber
|
||||
case isAlphaNumeric(r):
|
||||
l.backup()
|
||||
return lexName
|
||||
default:
|
||||
return l.errorf("unrecognized character in action: %#U", r)
|
||||
}
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
// lexName scans a name.
|
||||
func lexName(l *lexer) stateFn {
|
||||
for {
|
||||
r := l.next()
|
||||
|
||||
if r == eof {
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isAlphaNumeric(r) {
|
||||
l.backup()
|
||||
s, e := l.current()
|
||||
|
||||
switch {
|
||||
case equals(l.input, s, e, queryToken):
|
||||
l.emitL(itemQuery)
|
||||
case equals(l.input, s, e, mutationToken):
|
||||
l.emitL(itemMutation)
|
||||
case equals(l.input, s, e, subscriptionToken):
|
||||
l.emitL(itemSub)
|
||||
case equals(l.input, s, e, trueToken):
|
||||
l.emitL(itemBoolVal)
|
||||
case equals(l.input, s, e, falseToken):
|
||||
l.emitL(itemBoolVal)
|
||||
default:
|
||||
l.emit(itemName)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
// lexString scans a string.
|
||||
func lexString(l *lexer) stateFn {
|
||||
if l.accept([]byte(quotesToken)) {
|
||||
l.ignore()
|
||||
|
||||
for {
|
||||
r := l.next()
|
||||
if r == eof {
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
}
|
||||
if r == '\'' || r == '"' {
|
||||
l.backup()
|
||||
l.emit(itemStringVal)
|
||||
if l.accept(quotesToken) {
|
||||
l.ignore()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
||||
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
||||
// and "089" - but when it's wrong the input is invalid and the parser (via
|
||||
// strconv) will notice.
|
||||
func lexNumber(l *lexer) stateFn {
|
||||
var it itemType
|
||||
// Optional leading sign.
|
||||
l.accept(signsToken)
|
||||
|
||||
// Is it integer
|
||||
if l.accept(digitToken) {
|
||||
l.acceptRun(digitToken)
|
||||
it = itemIntVal
|
||||
}
|
||||
|
||||
// Is it float
|
||||
if l.peek() == '.' {
|
||||
if l.accept(dotToken) {
|
||||
if l.accept(digitToken) {
|
||||
l.acceptRun(digitToken)
|
||||
it = itemFloatVal
|
||||
}
|
||||
} else {
|
||||
l.backup()
|
||||
}
|
||||
}
|
||||
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if isAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
|
||||
if it != 0 {
|
||||
l.emit(it)
|
||||
}
|
||||
|
||||
return lexRoot
|
||||
}
|
||||
|
||||
// isSpace reports whether r is a space character.
|
||||
func isSpace(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
// isEndOfLine reports whether r is an end-of-line character.
|
||||
func isEndOfLine(r rune) bool {
|
||||
return r == '\r' || r == '\n' || r == eof
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
func equals(b []byte, s Pos, e Pos, val []byte) bool {
|
||||
n := 0
|
||||
for i := s; i < e; i++ {
|
||||
if n >= len(val) {
|
||||
return true
|
||||
}
|
||||
switch {
|
||||
case b[i] >= 'A' && b[i] <= 'Z' && ('a'+(b[i]-'A')) != val[n]:
|
||||
return false
|
||||
case b[i] != val[n]:
|
||||
return false
|
||||
}
|
||||
n++
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contains(b []byte, s Pos, e Pos, val []byte) bool {
|
||||
for i := s; i < e; i++ {
|
||||
for n := 0; n < len(val); n++ {
|
||||
if b[i] == val[n] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func lowercase(b []byte, s Pos, e Pos) {
|
||||
for i := s; i < e; i++ {
|
||||
if b[i] >= 'A' && b[i] <= 'Z' {
|
||||
b[i] = ('a' + (b[i] - 'A'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i item) String() string {
|
||||
var v string
|
||||
|
||||
switch i._type {
|
||||
case itemEOF:
|
||||
v = "EOF"
|
||||
case itemError:
|
||||
v = "error"
|
||||
case itemName:
|
||||
v = "name"
|
||||
case itemQuery:
|
||||
v = "query"
|
||||
case itemMutation:
|
||||
v = "mutation"
|
||||
case itemSub:
|
||||
v = "subscription"
|
||||
case itemPunctuator:
|
||||
v = "punctuator"
|
||||
case itemDirective:
|
||||
v = "directive"
|
||||
case itemVariable:
|
||||
v = "variable"
|
||||
case itemIntVal:
|
||||
v = "int"
|
||||
case itemFloatVal:
|
||||
v = "float"
|
||||
case itemStringVal:
|
||||
v = "string"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
611
core/internal/qcode/parse.go
Normal file
611
core/internal/qcode/parse.go
Normal file
@ -0,0 +1,611 @@
|
||||
package qcode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
)
|
||||
|
||||
var (
|
||||
errEOT = errors.New("end of tokens")
|
||||
)
|
||||
|
||||
type parserType int32
|
||||
|
||||
const (
|
||||
maxFields = 1200
|
||||
maxArgs = 25
|
||||
)
|
||||
|
||||
const (
|
||||
parserError parserType = iota
|
||||
parserEOF
|
||||
opQuery
|
||||
opMutate
|
||||
opSub
|
||||
NodeStr
|
||||
NodeInt
|
||||
NodeFloat
|
||||
NodeBool
|
||||
NodeObj
|
||||
NodeList
|
||||
NodeVar
|
||||
)
|
||||
|
||||
type Operation struct {
|
||||
Type parserType
|
||||
Name string
|
||||
Args []Arg
|
||||
argsA [10]Arg
|
||||
Fields []Field
|
||||
fieldsA [10]Field
|
||||
}
|
||||
|
||||
var zeroOperation = Operation{}
|
||||
|
||||
func (o *Operation) Reset() {
|
||||
*o = zeroOperation
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
ID int32
|
||||
ParentID int32
|
||||
Name string
|
||||
Alias string
|
||||
Args []Arg
|
||||
argsA [5]Arg
|
||||
Children []int32
|
||||
childrenA [5]int32
|
||||
}
|
||||
|
||||
type Arg struct {
|
||||
Name string
|
||||
Val *Node
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Type parserType
|
||||
Name string
|
||||
Val string
|
||||
Parent *Node
|
||||
Children []*Node
|
||||
exp *Exp
|
||||
}
|
||||
|
||||
var zeroNode = Node{}
|
||||
|
||||
func (n *Node) Reset() {
|
||||
*n = zeroNode
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
input []byte // the string being scanned
|
||||
pos int
|
||||
items []item
|
||||
err error
|
||||
}
|
||||
|
||||
var nodePool = sync.Pool{
|
||||
New: func() interface{} { return new(Node) },
|
||||
}
|
||||
|
||||
var opPool = sync.Pool{
|
||||
New: func() interface{} { return new(Operation) },
|
||||
}
|
||||
|
||||
var lexPool = sync.Pool{
|
||||
New: func() interface{} { return new(lexer) },
|
||||
}
|
||||
|
||||
func Parse(gql []byte) (*Operation, error) {
|
||||
return parseSelectionSet(gql)
|
||||
}
|
||||
|
||||
func ParseArgValue(argVal string) (*Node, error) {
|
||||
l := lexPool.Get().(*lexer)
|
||||
l.Reset()
|
||||
|
||||
if err := lex(l, []byte(argVal)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Parser{
|
||||
input: l.input,
|
||||
pos: -1,
|
||||
items: l.items,
|
||||
}
|
||||
op, err := p.parseValue()
|
||||
lexPool.Put(l)
|
||||
|
||||
return op, err
|
||||
}
|
||||
|
||||
func parseSelectionSet(gql []byte) (*Operation, error) {
|
||||
var err error
|
||||
|
||||
if len(gql) == 0 {
|
||||
return nil, errors.New("blank query")
|
||||
}
|
||||
|
||||
l := lexPool.Get().(*lexer)
|
||||
l.Reset()
|
||||
|
||||
if err = lex(l, gql); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Parser{
|
||||
input: l.input,
|
||||
pos: -1,
|
||||
items: l.items,
|
||||
}
|
||||
|
||||
var op *Operation
|
||||
|
||||
if p.peek(itemObjOpen) {
|
||||
p.ignore()
|
||||
op, err = p.parseQueryOp()
|
||||
} else {
|
||||
op, err = p.parseOp()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.peek(itemObjClose) {
|
||||
p.ignore()
|
||||
} else {
|
||||
return nil, fmt.Errorf("operation missing closing '}'")
|
||||
}
|
||||
|
||||
if !p.peek(itemEOF) {
|
||||
p.ignore()
|
||||
return nil, fmt.Errorf("invalid '%s' found after closing '}'", p.current())
|
||||
}
|
||||
|
||||
lexPool.Put(l)
|
||||
|
||||
return op, err
|
||||
}
|
||||
|
||||
func (p *Parser) next() item {
|
||||
n := p.pos + 1
|
||||
if n >= len(p.items) {
|
||||
p.err = errEOT
|
||||
return item{_type: itemEOF}
|
||||
}
|
||||
p.pos = n
|
||||
return p.items[p.pos]
|
||||
}
|
||||
|
||||
func (p *Parser) ignore() {
|
||||
n := p.pos + 1
|
||||
if n >= len(p.items) {
|
||||
p.err = errEOT
|
||||
return
|
||||
}
|
||||
p.pos = n
|
||||
}
|
||||
|
||||
func (p *Parser) current() string {
|
||||
item := p.items[p.pos]
|
||||
return b2s(p.input[item.pos:item.end])
|
||||
}
|
||||
|
||||
func (p *Parser) peek(types ...itemType) bool {
|
||||
n := p.pos + 1
|
||||
// if p.items[n]._type == itemEOF {
|
||||
// return false
|
||||
// }
|
||||
if n >= len(p.items) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(types); i++ {
|
||||
if p.items[n]._type == types[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) parseOp() (*Operation, error) {
|
||||
if !p.peek(itemQuery, itemMutation, itemSub) {
|
||||
err := errors.New("expecting a query, mutation or subscription")
|
||||
return nil, err
|
||||
}
|
||||
item := p.next()
|
||||
|
||||
op := opPool.Get().(*Operation)
|
||||
op.Reset()
|
||||
|
||||
switch item._type {
|
||||
case itemQuery:
|
||||
op.Type = opQuery
|
||||
case itemMutation:
|
||||
op.Type = opMutate
|
||||
case itemSub:
|
||||
op.Type = opSub
|
||||
}
|
||||
|
||||
op.Fields = op.fieldsA[:0]
|
||||
op.Args = op.argsA[:0]
|
||||
|
||||
var err error
|
||||
|
||||
if p.peek(itemName) {
|
||||
op.Name = p.val(p.next())
|
||||
}
|
||||
|
||||
if p.peek(itemArgsOpen) {
|
||||
p.ignore()
|
||||
|
||||
op.Args, err = p.parseOpParams(op.Args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p.peek(itemObjOpen) {
|
||||
p.ignore()
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
if !p.peek(itemName) {
|
||||
break
|
||||
}
|
||||
|
||||
op.Fields, err = p.parseFields(op.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseQueryOp() (*Operation, error) {
|
||||
op := opPool.Get().(*Operation)
|
||||
op.Reset()
|
||||
|
||||
op.Type = opQuery
|
||||
op.Fields = op.fieldsA[:0]
|
||||
op.Args = op.argsA[:0]
|
||||
|
||||
var err error
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
if !p.peek(itemName) {
|
||||
break
|
||||
}
|
||||
|
||||
op.Fields, err = p.parseFields(op.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFields(fields []Field) ([]Field, error) {
|
||||
st := util.NewStack()
|
||||
|
||||
for {
|
||||
if len(fields) >= maxFields {
|
||||
return nil, fmt.Errorf("too many fields (max %d)", maxFields)
|
||||
}
|
||||
|
||||
if p.peek(itemObjClose) {
|
||||
p.ignore()
|
||||
st.Pop()
|
||||
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an alias or field name")
|
||||
}
|
||||
|
||||
fields = append(fields, Field{ID: int32(len(fields))})
|
||||
|
||||
f := &fields[(len(fields) - 1)]
|
||||
f.Args = f.argsA[:0]
|
||||
f.Children = f.childrenA[:0]
|
||||
|
||||
// Parse the inside of the the fields () parentheses
|
||||
// in short parse the args like id, where, etc
|
||||
if err := p.parseField(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
intf := st.Peek()
|
||||
if pid, ok := intf.(int32); ok {
|
||||
f.ParentID = pid
|
||||
fields[pid].Children = append(fields[pid].Children, f.ID)
|
||||
} else {
|
||||
f.ParentID = -1
|
||||
}
|
||||
|
||||
// The first opening curley brackets after this
|
||||
// comes the columns or child fields
|
||||
if p.peek(itemObjOpen) {
|
||||
p.ignore()
|
||||
st.Push(f.ID)
|
||||
|
||||
} else if p.peek(itemObjClose) {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseField(f *Field) error {
|
||||
var err error
|
||||
v := p.next()
|
||||
|
||||
if p.peek(itemColon) {
|
||||
p.ignore()
|
||||
|
||||
if p.peek(itemName) {
|
||||
f.Alias = p.val(v)
|
||||
f.Name = p.vall(p.next())
|
||||
} else {
|
||||
return errors.New("expecting an aliased field name")
|
||||
}
|
||||
} else {
|
||||
f.Name = p.vall(v)
|
||||
}
|
||||
|
||||
if p.peek(itemArgsOpen) {
|
||||
p.ignore()
|
||||
if f.Args, err = p.parseArgs(f.Args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseOpParams(args []Arg) ([]Arg, error) {
|
||||
for {
|
||||
if len(args) >= maxArgs {
|
||||
return nil, fmt.Errorf("too many args (max %d)", maxArgs)
|
||||
}
|
||||
|
||||
if p.peek(itemArgsClose) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
p.next()
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseArgs(args []Arg) ([]Arg, error) {
|
||||
var err error
|
||||
|
||||
for {
|
||||
if len(args) >= maxArgs {
|
||||
return nil, fmt.Errorf("too many args (max %d)", maxArgs)
|
||||
}
|
||||
|
||||
if p.peek(itemArgsClose) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an argument name")
|
||||
}
|
||||
args = append(args, Arg{Name: p.val(p.next())})
|
||||
arg := &args[(len(args) - 1)]
|
||||
|
||||
if !p.peek(itemColon) {
|
||||
return nil, errors.New("missing ':' after argument name")
|
||||
}
|
||||
p.ignore()
|
||||
|
||||
arg.Val, err = p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseList() (*Node, error) {
|
||||
nodes := []*Node{}
|
||||
|
||||
parent := nodePool.Get().(*Node)
|
||||
parent.Reset()
|
||||
|
||||
var ty parserType
|
||||
for {
|
||||
if p.peek(itemListClose) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
node, err := p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ty == 0 {
|
||||
ty = node.Type
|
||||
} else {
|
||||
if ty != node.Type {
|
||||
return nil, errors.New("All values in a list must be of the same type")
|
||||
}
|
||||
}
|
||||
node.Parent = parent
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return nil, errors.New("List cannot be empty")
|
||||
}
|
||||
|
||||
parent.Type = NodeList
|
||||
parent.Children = nodes
|
||||
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseObj() (*Node, error) {
|
||||
nodes := []*Node{}
|
||||
|
||||
parent := nodePool.Get().(*Node)
|
||||
parent.Reset()
|
||||
|
||||
for {
|
||||
if p.peek(itemObjClose) {
|
||||
p.ignore()
|
||||
break
|
||||
}
|
||||
|
||||
if !p.peek(itemName) {
|
||||
return nil, errors.New("expecting an argument name")
|
||||
}
|
||||
nodeName := p.val(p.next())
|
||||
|
||||
if !p.peek(itemColon) {
|
||||
return nil, errors.New("missing ':' after Field argument name")
|
||||
}
|
||||
p.ignore()
|
||||
|
||||
node, err := p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Name = nodeName
|
||||
node.Parent = parent
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
parent.Type = NodeObj
|
||||
parent.Children = nodes
|
||||
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseValue() (*Node, error) {
|
||||
if p.peek(itemListOpen) {
|
||||
p.ignore()
|
||||
return p.parseList()
|
||||
}
|
||||
|
||||
if p.peek(itemObjOpen) {
|
||||
p.ignore()
|
||||
return p.parseObj()
|
||||
}
|
||||
|
||||
item := p.next()
|
||||
node := nodePool.Get().(*Node)
|
||||
node.Reset()
|
||||
|
||||
switch item._type {
|
||||
case itemIntVal:
|
||||
node.Type = NodeInt
|
||||
case itemFloatVal:
|
||||
node.Type = NodeFloat
|
||||
case itemStringVal:
|
||||
node.Type = NodeStr
|
||||
case itemBoolVal:
|
||||
node.Type = NodeBool
|
||||
case itemName:
|
||||
node.Type = NodeStr
|
||||
case itemVariable:
|
||||
node.Type = NodeVar
|
||||
default:
|
||||
return nil, fmt.Errorf("expecting a number, string, object, list or variable as an argument value (not %s)", p.val(p.next()))
|
||||
}
|
||||
node.Val = p.val(item)
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (p *Parser) val(v item) string {
|
||||
return b2s(p.input[v.pos:v.end])
|
||||
}
|
||||
|
||||
func (p *Parser) vall(v item) string {
|
||||
lowercase(p.input, v.pos, v.end)
|
||||
return b2s(p.input[v.pos:v.end])
|
||||
}
|
||||
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
func (t parserType) String() string {
|
||||
var v string
|
||||
|
||||
switch t {
|
||||
case parserEOF:
|
||||
v = "EOF"
|
||||
case parserError:
|
||||
v = "error"
|
||||
case opQuery:
|
||||
v = "query"
|
||||
case opMutate:
|
||||
v = "mutation"
|
||||
case opSub:
|
||||
v = "subscription"
|
||||
case NodeStr:
|
||||
v = "node-string"
|
||||
case NodeInt:
|
||||
v = "node-int"
|
||||
case NodeFloat:
|
||||
v = "node-float"
|
||||
case NodeBool:
|
||||
v = "node-bool"
|
||||
case NodeVar:
|
||||
v = "node-var"
|
||||
case NodeObj:
|
||||
v = "node-obj"
|
||||
case NodeList:
|
||||
v = "node-list"
|
||||
}
|
||||
return fmt.Sprintf("<%s>", v)
|
||||
}
|
||||
|
||||
// type Frees struct {
|
||||
// n *Node
|
||||
// loc int
|
||||
// }
|
||||
|
||||
// var freeList []Frees
|
||||
|
||||
// func FreeNode(n *Node, loc int) {
|
||||
// j := -1
|
||||
|
||||
// for i := range freeList {
|
||||
// if n == freeList[i].n {
|
||||
// j = i
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if j == -1 {
|
||||
// nodePool.Put(n)
|
||||
// freeList = append(freeList, Frees{n, loc})
|
||||
// } else {
|
||||
// fmt.Printf(">>>>(%d) RE_FREE %d %p %s %s\n", loc, freeList[j].loc, freeList[j].n, n.Name, n.Type)
|
||||
// }
|
||||
// }
|
||||
|
||||
func FreeNode(n *Node, loc int) {
|
||||
nodePool.Put(n)
|
||||
}
|
183
core/internal/qcode/parse_test.go
Normal file
183
core/internal/qcode/parse_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
package qcode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompile1(t *testing.T) {
|
||||
qc, _ := NewCompiler(Config{})
|
||||
err := qc.AddRole("user", "product", TRConfig{
|
||||
Query: QueryConfig{
|
||||
Columns: []string{"id", "Name"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = qc.Compile([]byte(`
|
||||
query { product(id: 15) {
|
||||
id
|
||||
name
|
||||
} }`), "user")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal(errors.New("this should be an error id must be a variable"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile2(t *testing.T) {
|
||||
qc, _ := NewCompiler(Config{})
|
||||
err := qc.AddRole("user", "product", TRConfig{
|
||||
Query: QueryConfig{
|
||||
Columns: []string{"ID"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = qc.Compile([]byte(`
|
||||
query { product(id: $id) {
|
||||
id
|
||||
name
|
||||
} }`), "user")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile3(t *testing.T) {
|
||||
qc, _ := NewCompiler(Config{})
|
||||
err := qc.AddRole("user", "product", TRConfig{
|
||||
Query: QueryConfig{
|
||||
Columns: []string{"ID"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = qc.Compile([]byte(`
|
||||
mutation {
|
||||
product(id: $test, name: "Test") {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`), "user")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCompile1(t *testing.T) {
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
_, err := qcompile.Compile([]byte(`#`), "user")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal(errors.New("expecting an error"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCompile2(t *testing.T) {
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
_, err := qcompile.Compile([]byte(`{u(where:{not:0})}`), "user")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal(errors.New("expecting an error"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyCompile(t *testing.T) {
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
_, err := qcompile.Compile([]byte(``), "user")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal(errors.New("expecting an error"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPostfixCompile(t *testing.T) {
|
||||
gql := `mutation
|
||||
updateThread {
|
||||
thread(update: $data, where: { slug: { eq: $slug } }) {
|
||||
slug
|
||||
title
|
||||
published
|
||||
createdAt : created_at
|
||||
totalVotes : cached_votes_total
|
||||
totalPosts : cached_posts_total
|
||||
vote : thread_vote(where: { user_id: { eq: $user_id } }) {
|
||||
id
|
||||
}
|
||||
topics {
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
_, err := qcompile.Compile([]byte(gql), "anon")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal(errors.New("expecting an error"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var gql = []byte(`
|
||||
products(
|
||||
# returns only 30 items
|
||||
limit: 30,
|
||||
|
||||
# starts from item 10, commented out for now
|
||||
# offset: 10,
|
||||
|
||||
# orders the response items by highest price
|
||||
order_by: { price: desc },
|
||||
|
||||
# no duplicate prices returned
|
||||
distinct: [ price ]
|
||||
|
||||
# only items with an id >= 30 and < 30 are returned
|
||||
where: { id: { AND: { greater_or_equals: 20, lt: 28 } } }) {
|
||||
id
|
||||
name
|
||||
price
|
||||
}`)
|
||||
|
||||
func BenchmarkQCompile(b *testing.B) {
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := qcompile.Compile(gql, "user")
|
||||
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQCompileP(b *testing.B) {
|
||||
qcompile, _ := NewCompiler(Config{})
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, err := qcompile.Compile(gql, "user")
|
||||
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
3
core/internal/qcode/pprof_cpu.sh
Executable file
3
core/internal/qcode/pprof_cpu.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
go test -bench=. -benchmem -cpuprofile cpu.out -run=XXX
|
||||
go tool pprof -cum cpu.out
|
3
core/internal/qcode/pprof_mem.sh
Executable file
3
core/internal/qcode/pprof_mem.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
go test -bench=. -benchmem -memprofile mem.out -run=XXX
|
||||
go tool pprof -cum mem.out
|
1177
core/internal/qcode/qcode.go
Normal file
1177
core/internal/qcode/qcode.go
Normal file
@ -0,0 +1,1177 @@
|
||||
package qcode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dosco/super-graph/core/internal/util"
|
||||
"github.com/gobuffalo/flect"
|
||||
)
|
||||
|
||||
type QType int
|
||||
type Action int
|
||||
|
||||
const (
|
||||
maxSelectors = 30
|
||||
)
|
||||
|
||||
const (
|
||||
QTQuery QType = iota + 1
|
||||
QTMutation
|
||||
QTInsert
|
||||
QTUpdate
|
||||
QTDelete
|
||||
QTUpsert
|
||||
)
|
||||
|
||||
type QCode struct {
|
||||
Type QType
|
||||
ActionVar string
|
||||
Selects []Select
|
||||
Roots []int32
|
||||
rootsA [5]int32
|
||||
}
|
||||
|
||||
type Select struct {
|
||||
ID int32
|
||||
ParentID int32
|
||||
Args map[string]*Node
|
||||
Name string
|
||||
FieldName string
|
||||
Cols []Column
|
||||
Where *Exp
|
||||
OrderBy []*OrderBy
|
||||
DistinctOn []string
|
||||
Paging Paging
|
||||
Children []int32
|
||||
Functions bool
|
||||
Allowed map[string]struct{}
|
||||
PresetMap map[string]string
|
||||
PresetList []string
|
||||
SkipRender bool
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
Table string
|
||||
Name string
|
||||
FieldName string
|
||||
}
|
||||
|
||||
type Exp struct {
|
||||
Op ExpOp
|
||||
Col string
|
||||
NestedCols []string
|
||||
Type ValType
|
||||
Table string
|
||||
Val string
|
||||
ListType ValType
|
||||
ListVal []string
|
||||
Children []*Exp
|
||||
childrenA [5]*Exp
|
||||
doFree bool
|
||||
}
|
||||
|
||||
var zeroExp = Exp{doFree: true}
|
||||
|
||||
func (ex *Exp) Reset() {
|
||||
*ex = zeroExp
|
||||
}
|
||||
|
||||
type OrderBy struct {
|
||||
Col string
|
||||
Order Order
|
||||
}
|
||||
|
||||
type PagingType int
|
||||
|
||||
const (
|
||||
PtOffset PagingType = iota
|
||||
PtForward
|
||||
PtBackward
|
||||
)
|
||||
|
||||
type Paging struct {
|
||||
Type PagingType
|
||||
Limit string
|
||||
Offset string
|
||||
Cursor bool
|
||||
NoLimit bool
|
||||
}
|
||||
|
||||
type ExpOp int
|
||||
|
||||
const (
|
||||
OpNop ExpOp = iota
|
||||
OpAnd
|
||||
OpOr
|
||||
OpNot
|
||||
OpEquals
|
||||
OpNotEquals
|
||||
OpGreaterOrEquals
|
||||
OpLesserOrEquals
|
||||
OpGreaterThan
|
||||
OpLesserThan
|
||||
OpIn
|
||||
OpNotIn
|
||||
OpLike
|
||||
OpNotLike
|
||||
OpILike
|
||||
OpNotILike
|
||||
OpSimilar
|
||||
OpNotSimilar
|
||||
OpContains
|
||||
OpContainedIn
|
||||
OpHasKey
|
||||
OpHasKeyAny
|
||||
OpHasKeyAll
|
||||
OpIsNull
|
||||
OpEqID
|
||||
OpTsQuery
|
||||
OpFalse
|
||||
OpNotDistinct
|
||||
OpDistinct
|
||||
)
|
||||
|
||||
type ValType int
|
||||
|
||||
const (
|
||||
ValStr ValType = iota + 1
|
||||
ValInt
|
||||
ValFloat
|
||||
ValBool
|
||||
ValList
|
||||
ValVar
|
||||
ValNone
|
||||
ValRef
|
||||
)
|
||||
|
||||
type AggregrateOp int
|
||||
|
||||
const (
|
||||
AgCount AggregrateOp = iota + 1
|
||||
AgSum
|
||||
AgAvg
|
||||
AgMax
|
||||
AgMin
|
||||
)
|
||||
|
||||
type Order int
|
||||
|
||||
const (
|
||||
OrderAsc Order = iota + 1
|
||||
OrderDesc
|
||||
OrderAscNullsFirst
|
||||
OrderAscNullsLast
|
||||
OrderDescNullsFirst
|
||||
OrderDescNullsLast
|
||||
)
|
||||
|
||||
type Compiler struct {
|
||||
tr map[string]map[string]*trval
|
||||
bl map[string]struct{}
|
||||
}
|
||||
|
||||
var expPool = sync.Pool{
|
||||
New: func() interface{} { return &Exp{doFree: true} },
|
||||
}
|
||||
|
||||
func NewCompiler(c Config) (*Compiler, error) {
|
||||
co := &Compiler{}
|
||||
co.tr = make(map[string]map[string]*trval)
|
||||
co.bl = make(map[string]struct{}, len(c.Blocklist))
|
||||
|
||||
for i := range c.Blocklist {
|
||||
co.bl[strings.ToLower(c.Blocklist[i])] = struct{}{}
|
||||
}
|
||||
|
||||
seedExp := [100]Exp{}
|
||||
|
||||
for i := range seedExp {
|
||||
seedExp[i].doFree = true
|
||||
expPool.Put(&seedExp[i])
|
||||
}
|
||||
|
||||
return co, nil
|
||||
}
|
||||
|
||||
func NewFilter() *Exp {
|
||||
ex := expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
func (com *Compiler) AddRole(role, table string, trc TRConfig) error {
|
||||
var err error
|
||||
trv := &trval{}
|
||||
|
||||
// query config
|
||||
trv.query.fil, trv.query.filNU, err = compileFilter(trc.Query.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if trc.Query.Limit > 0 {
|
||||
trv.query.limit = strconv.Itoa(trc.Query.Limit)
|
||||
}
|
||||
trv.query.cols = listToMap(trc.Query.Columns)
|
||||
trv.query.disable.funcs = trc.Query.DisableFunctions
|
||||
|
||||
// insert config
|
||||
trv.insert.fil, trv.insert.filNU, err = compileFilter(trc.Insert.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trv.insert.cols = listToMap(trc.Insert.Columns)
|
||||
trv.insert.psmap = parsePresets(trc.Insert.Presets)
|
||||
trv.insert.pslist = mapToList(trv.insert.psmap)
|
||||
|
||||
// update config
|
||||
trv.update.fil, trv.update.filNU, err = compileFilter(trc.Update.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trv.update.cols = listToMap(trc.Update.Columns)
|
||||
trv.update.psmap = parsePresets(trc.Update.Presets)
|
||||
trv.update.pslist = mapToList(trv.update.psmap)
|
||||
|
||||
// delete config
|
||||
trv.delete.fil, trv.delete.filNU, err = compileFilter(trc.Delete.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trv.delete.cols = listToMap(trc.Delete.Columns)
|
||||
|
||||
singular := flect.Singularize(table)
|
||||
plural := flect.Pluralize(table)
|
||||
|
||||
if _, ok := com.tr[role]; !ok {
|
||||
com.tr[role] = make(map[string]*trval)
|
||||
}
|
||||
|
||||
com.tr[role][singular] = trv
|
||||
com.tr[role][plural] = trv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (com *Compiler) Compile(query []byte, role string) (*QCode, error) {
|
||||
var err error
|
||||
|
||||
qc := QCode{Type: QTQuery}
|
||||
qc.Roots = qc.rootsA[:0]
|
||||
|
||||
op, err := Parse(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = com.compileQuery(&qc, op, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opPool.Put(op)
|
||||
|
||||
return &qc, nil
|
||||
}
|
||||
|
||||
func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||
id := int32(0)
|
||||
|
||||
if len(op.Fields) == 0 {
|
||||
return errors.New("invalid graphql no query found")
|
||||
}
|
||||
|
||||
if op.Type == opMutate {
|
||||
if err := com.setMutationType(qc, op.Fields[0].Args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
selects := make([]Select, 0, 5)
|
||||
st := NewStack()
|
||||
action := qc.Type
|
||||
|
||||
if len(op.Fields) == 0 {
|
||||
return errors.New("empty query")
|
||||
}
|
||||
|
||||
for i := range op.Fields {
|
||||
if op.Fields[i].ParentID == -1 {
|
||||
val := op.Fields[i].ID | (-1 << 16)
|
||||
st.Push(val)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if id >= maxSelectors {
|
||||
return fmt.Errorf("selector limit reached (%d)", maxSelectors)
|
||||
}
|
||||
|
||||
val := st.Pop()
|
||||
fid := val & 0xFFFF
|
||||
parentID := (val >> 16) & 0xFFFF
|
||||
|
||||
field := &op.Fields[fid]
|
||||
|
||||
if _, ok := com.bl[field.Name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.ParentID == -1 {
|
||||
parentID = -1
|
||||
}
|
||||
|
||||
trv := com.getRole(role, field.Name)
|
||||
|
||||
selects = append(selects, Select{
|
||||
ID: id,
|
||||
ParentID: parentID,
|
||||
Name: field.Name,
|
||||
Children: make([]int32, 0, 5),
|
||||
Allowed: trv.allowedColumns(action),
|
||||
Functions: true,
|
||||
})
|
||||
s := &selects[(len(selects) - 1)]
|
||||
|
||||
switch action {
|
||||
case QTQuery:
|
||||
s.Functions = !trv.query.disable.funcs
|
||||
s.Paging.Limit = trv.query.limit
|
||||
|
||||
case QTInsert:
|
||||
s.PresetMap = trv.insert.psmap
|
||||
s.PresetList = trv.insert.pslist
|
||||
|
||||
case QTUpdate:
|
||||
s.PresetMap = trv.update.psmap
|
||||
s.PresetList = trv.update.pslist
|
||||
}
|
||||
|
||||
if len(field.Alias) != 0 {
|
||||
s.FieldName = field.Alias
|
||||
} else {
|
||||
s.FieldName = s.Name
|
||||
}
|
||||
|
||||
err := com.compileArgs(qc, s, field.Args, role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Order is important AddFilters must come after compileArgs
|
||||
com.AddFilters(qc, s, role)
|
||||
|
||||
if s.ParentID == -1 {
|
||||
qc.Roots = append(qc.Roots, s.ID)
|
||||
} else {
|
||||
p := &selects[s.ParentID]
|
||||
p.Children = append(p.Children, s.ID)
|
||||
}
|
||||
|
||||
s.Cols = make([]Column, 0, len(field.Children))
|
||||
action = QTQuery
|
||||
|
||||
for _, cid := range field.Children {
|
||||
f := op.Fields[cid]
|
||||
|
||||
if _, ok := com.bl[f.Name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(f.Children) != 0 {
|
||||
val := f.ID | (s.ID << 16)
|
||||
st.Push(val)
|
||||
continue
|
||||
}
|
||||
|
||||
col := Column{Name: f.Name}
|
||||
|
||||
if len(f.Alias) != 0 {
|
||||
col.FieldName = f.Alias
|
||||
} else {
|
||||
col.FieldName = f.Name
|
||||
}
|
||||
s.Cols = append(s.Cols, col)
|
||||
}
|
||||
|
||||
id++
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
return errors.New("invalid query")
|
||||
}
|
||||
|
||||
qc.Selects = selects[:id]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) {
|
||||
var fil *Exp
|
||||
var nu bool
|
||||
|
||||
if trv, ok := com.tr[role][sel.Name]; ok {
|
||||
fil, nu = trv.filter(qc.Type)
|
||||
|
||||
} else if role == "anon" {
|
||||
// Tables not defined under the anon role will not be rendered
|
||||
sel.SkipRender = true
|
||||
}
|
||||
|
||||
if fil == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if nu && role == "anon" {
|
||||
sel.SkipRender = true
|
||||
}
|
||||
|
||||
switch fil.Op {
|
||||
case OpNop:
|
||||
case OpFalse:
|
||||
sel.Where = fil
|
||||
default:
|
||||
AddFilter(sel, fil)
|
||||
}
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgs(qc *QCode, sel *Select, args []Arg, role string) error {
|
||||
var err error
|
||||
|
||||
// don't free this arg either previously done or will be free'd
|
||||
// in the future like in psql
|
||||
var df bool
|
||||
|
||||
for i := range args {
|
||||
arg := &args[i]
|
||||
|
||||
switch arg.Name {
|
||||
case "id":
|
||||
err, df = com.compileArgID(sel, arg)
|
||||
|
||||
case "search":
|
||||
err, df = com.compileArgSearch(sel, arg)
|
||||
|
||||
case "where":
|
||||
err, df = com.compileArgWhere(sel, arg, role)
|
||||
|
||||
case "orderby", "order_by", "order":
|
||||
err, df = com.compileArgOrderBy(sel, arg)
|
||||
|
||||
case "distinct_on", "distinct":
|
||||
err, df = com.compileArgDistinctOn(sel, arg)
|
||||
|
||||
case "limit":
|
||||
err, df = com.compileArgLimit(sel, arg)
|
||||
|
||||
case "offset":
|
||||
err, df = com.compileArgOffset(sel, arg)
|
||||
|
||||
case "first":
|
||||
err, df = com.compileArgFirstLast(sel, arg, PtForward)
|
||||
|
||||
case "last":
|
||||
err, df = com.compileArgFirstLast(sel, arg, PtBackward)
|
||||
|
||||
case "after":
|
||||
err, df = com.compileArgAfterBefore(sel, arg, PtForward)
|
||||
|
||||
case "before":
|
||||
err, df = com.compileArgAfterBefore(sel, arg, PtBackward)
|
||||
}
|
||||
|
||||
if !df {
|
||||
FreeNode(arg.Val, 5)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (com *Compiler) setMutationType(qc *QCode, args []Arg) error {
|
||||
setActionVar := func(arg *Arg) error {
|
||||
if arg.Val.Type != NodeVar {
|
||||
return argErr(arg.Name, "variable")
|
||||
}
|
||||
qc.ActionVar = arg.Val.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range args {
|
||||
arg := &args[i]
|
||||
|
||||
switch arg.Name {
|
||||
case "insert":
|
||||
qc.Type = QTInsert
|
||||
return setActionVar(arg)
|
||||
case "update":
|
||||
qc.Type = QTUpdate
|
||||
return setActionVar(arg)
|
||||
case "upsert":
|
||||
qc.Type = QTUpsert
|
||||
return setActionVar(arg)
|
||||
case "delete":
|
||||
qc.Type = QTDelete
|
||||
|
||||
if arg.Val.Type != NodeBool {
|
||||
return argErr(arg.Name, "boolen")
|
||||
}
|
||||
|
||||
if arg.Val.Val == "false" {
|
||||
qc.Type = QTQuery
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgObj(st *util.Stack, arg *Arg) (*Exp, bool, error) {
|
||||
if arg.Val.Type != NodeObj {
|
||||
return nil, false, fmt.Errorf("expecting an object")
|
||||
}
|
||||
|
||||
return com.compileArgNode(st, arg.Val, true)
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgNode(st *util.Stack, node *Node, usePool bool) (*Exp, bool, error) {
|
||||
var root *Exp
|
||||
var needsUser bool
|
||||
|
||||
if node == nil || len(node.Children) == 0 {
|
||||
return nil, false, errors.New("invalid argument value")
|
||||
}
|
||||
|
||||
pushChild(st, nil, node)
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
intf := st.Pop()
|
||||
|
||||
node, ok := intf.(*Node)
|
||||
if !ok || node == nil {
|
||||
return nil, needsUser, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
|
||||
}
|
||||
|
||||
// Objects inside a list
|
||||
if len(node.Name) == 0 {
|
||||
pushChildren(st, node.exp, node)
|
||||
continue
|
||||
|
||||
} else {
|
||||
if _, ok := com.bl[node.Name]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ex, err := newExp(st, node, usePool)
|
||||
if err != nil {
|
||||
return nil, needsUser, err
|
||||
}
|
||||
|
||||
if ex == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if ex.Type == ValVar && ex.Val == "user_id" {
|
||||
needsUser = true
|
||||
}
|
||||
|
||||
if node.exp == nil {
|
||||
root = ex
|
||||
} else {
|
||||
node.exp.Children = append(node.exp.Children, ex)
|
||||
}
|
||||
}
|
||||
|
||||
if usePool {
|
||||
st.Push(node)
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
intf := st.Pop()
|
||||
node, ok := intf.(*Node)
|
||||
if !ok || node == nil {
|
||||
continue
|
||||
}
|
||||
for i := range node.Children {
|
||||
st.Push(node.Children[i])
|
||||
}
|
||||
FreeNode(node, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return root, needsUser, nil
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgID(sel *Select, arg *Arg) (error, bool) {
|
||||
if sel.ID != 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if sel.Where != nil && sel.Where.Op == OpEqID {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if arg.Val.Type != NodeVar {
|
||||
return argErr("id", "variable"), false
|
||||
}
|
||||
|
||||
ex := expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
|
||||
ex.Op = OpEqID
|
||||
ex.Type = ValVar
|
||||
ex.Val = arg.Val.Val
|
||||
|
||||
sel.Where = ex
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) (error, bool) {
|
||||
if arg.Val.Type != NodeVar {
|
||||
return argErr("search", "variable"), false
|
||||
}
|
||||
|
||||
ex := expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
|
||||
ex.Op = OpTsQuery
|
||||
ex.Type = ValVar
|
||||
ex.Val = arg.Val.Val
|
||||
|
||||
if sel.Args == nil {
|
||||
sel.Args = make(map[string]*Node)
|
||||
}
|
||||
|
||||
sel.Args[arg.Name] = arg.Val
|
||||
AddFilter(sel, ex)
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg, role string) (error, bool) {
|
||||
st := util.NewStack()
|
||||
var err error
|
||||
|
||||
ex, nu, err := com.compileArgObj(st, arg)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
if nu && role == "anon" {
|
||||
sel.SkipRender = true
|
||||
}
|
||||
AddFilter(sel, ex)
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) (error, bool) {
|
||||
if arg.Val.Type != NodeObj {
|
||||
return fmt.Errorf("expecting an object"), false
|
||||
}
|
||||
|
||||
st := util.NewStack()
|
||||
|
||||
for i := range arg.Val.Children {
|
||||
st.Push(arg.Val.Children[i])
|
||||
}
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
intf := st.Pop()
|
||||
node, ok := intf.(*Node)
|
||||
|
||||
if !ok || node == nil {
|
||||
return fmt.Errorf("17: unexpected value %v (%t)", intf, intf), false
|
||||
}
|
||||
|
||||
if _, ok := com.bl[node.Name]; ok {
|
||||
FreeNode(node, 2)
|
||||
continue
|
||||
}
|
||||
|
||||
if node.Type != NodeStr && node.Type != NodeVar {
|
||||
return fmt.Errorf("expecting a string or variable"), false
|
||||
}
|
||||
|
||||
ob := &OrderBy{}
|
||||
|
||||
switch node.Val {
|
||||
case "asc":
|
||||
ob.Order = OrderAsc
|
||||
case "desc":
|
||||
ob.Order = OrderDesc
|
||||
case "asc_nulls_first":
|
||||
ob.Order = OrderAscNullsFirst
|
||||
case "desc_nulls_first":
|
||||
ob.Order = OrderDescNullsFirst
|
||||
case "asc_nulls_last":
|
||||
ob.Order = OrderAscNullsLast
|
||||
case "desc_nulls_last":
|
||||
ob.Order = OrderDescNullsLast
|
||||
default:
|
||||
return fmt.Errorf("valid values include asc, desc, asc_nulls_first and desc_nulls_first"), false
|
||||
}
|
||||
|
||||
setOrderByColName(ob, node)
|
||||
sel.OrderBy = append(sel.OrderBy, ob)
|
||||
FreeNode(node, 3)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if _, ok := com.bl[node.Name]; ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if node.Type != NodeList && node.Type != NodeStr {
|
||||
return fmt.Errorf("expecting a list of strings or just a string"), false
|
||||
}
|
||||
|
||||
if node.Type == NodeStr {
|
||||
sel.DistinctOn = append(sel.DistinctOn, node.Val)
|
||||
}
|
||||
|
||||
for i := range node.Children {
|
||||
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
|
||||
FreeNode(node.Children[i], 5)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeInt {
|
||||
return argErr("limit", "number"), false
|
||||
}
|
||||
|
||||
sel.Paging.Limit = node.Val
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeVar {
|
||||
return argErr("offset", "variable"), false
|
||||
}
|
||||
|
||||
sel.Paging.Offset = node.Val
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgFirstLast(sel *Select, arg *Arg, pt PagingType) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeInt {
|
||||
return argErr(arg.Name, "number"), false
|
||||
}
|
||||
|
||||
sel.Paging.Type = pt
|
||||
sel.Paging.Limit = node.Val
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) (error, bool) {
|
||||
node := arg.Val
|
||||
|
||||
if node.Type != NodeVar || node.Val != "cursor" {
|
||||
return fmt.Errorf("value for argument '%s' must be a variable named $cursor", arg.Name), false
|
||||
}
|
||||
sel.Paging.Type = pt
|
||||
sel.Paging.Cursor = true
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var zeroTrv = &trval{}
|
||||
|
||||
func (com *Compiler) getRole(role, field string) *trval {
|
||||
if trv, ok := com.tr[role][field]; ok {
|
||||
return trv
|
||||
} else {
|
||||
return zeroTrv
|
||||
}
|
||||
}
|
||||
|
||||
func AddFilter(sel *Select, fil *Exp) {
|
||||
if sel.Where != nil {
|
||||
ow := sel.Where
|
||||
|
||||
if sel.Where.Op != OpAnd || !sel.Where.doFree {
|
||||
sel.Where = expPool.Get().(*Exp)
|
||||
sel.Where.Reset()
|
||||
sel.Where.Op = OpAnd
|
||||
sel.Where.Children = sel.Where.childrenA[:2]
|
||||
sel.Where.Children[0] = fil
|
||||
sel.Where.Children[1] = ow
|
||||
|
||||
} else {
|
||||
sel.Where.Children = append(sel.Where.Children, fil)
|
||||
}
|
||||
|
||||
} else {
|
||||
sel.Where = fil
|
||||
}
|
||||
}
|
||||
|
||||
func newExp(st *util.Stack, node *Node, usePool bool) (*Exp, error) {
|
||||
name := node.Name
|
||||
if name[0] == '_' {
|
||||
name = name[1:]
|
||||
}
|
||||
|
||||
var ex *Exp
|
||||
|
||||
if usePool {
|
||||
ex = expPool.Get().(*Exp)
|
||||
ex.Reset()
|
||||
} else {
|
||||
ex = &Exp{doFree: false}
|
||||
}
|
||||
|
||||
ex.Children = ex.childrenA[:0]
|
||||
|
||||
switch name {
|
||||
case "and":
|
||||
if len(node.Children) == 0 {
|
||||
return nil, errors.New("missing expression after 'AND' operator")
|
||||
}
|
||||
ex.Op = OpAnd
|
||||
pushChildren(st, ex, node)
|
||||
case "or":
|
||||
if len(node.Children) == 0 {
|
||||
return nil, errors.New("missing expression after 'OR' operator")
|
||||
}
|
||||
ex.Op = OpOr
|
||||
pushChildren(st, ex, node)
|
||||
case "not":
|
||||
if len(node.Children) == 0 {
|
||||
return nil, errors.New("missing expression after 'NOT' operator")
|
||||
}
|
||||
ex.Op = OpNot
|
||||
pushChild(st, ex, node)
|
||||
case "eq", "equals":
|
||||
ex.Op = OpEquals
|
||||
ex.Val = node.Val
|
||||
case "neq", "not_equals":
|
||||
ex.Op = OpNotEquals
|
||||
ex.Val = node.Val
|
||||
case "gt", "greater_than":
|
||||
ex.Op = OpGreaterThan
|
||||
ex.Val = node.Val
|
||||
case "lt", "lesser_than":
|
||||
ex.Op = OpLesserThan
|
||||
ex.Val = node.Val
|
||||
case "gte", "greater_or_equals":
|
||||
ex.Op = OpGreaterOrEquals
|
||||
ex.Val = node.Val
|
||||
case "lte", "lesser_or_equals":
|
||||
ex.Op = OpLesserOrEquals
|
||||
ex.Val = node.Val
|
||||
case "in":
|
||||
ex.Op = OpIn
|
||||
setListVal(ex, node)
|
||||
case "nin", "not_in":
|
||||
ex.Op = OpNotIn
|
||||
setListVal(ex, node)
|
||||
case "like":
|
||||
ex.Op = OpLike
|
||||
ex.Val = node.Val
|
||||
case "nlike", "not_like":
|
||||
ex.Op = OpNotLike
|
||||
ex.Val = node.Val
|
||||
case "ilike":
|
||||
ex.Op = OpILike
|
||||
ex.Val = node.Val
|
||||
case "nilike", "not_ilike":
|
||||
ex.Op = OpILike
|
||||
ex.Val = node.Val
|
||||
case "similar":
|
||||
ex.Op = OpSimilar
|
||||
ex.Val = node.Val
|
||||
case "nsimilar", "not_similar":
|
||||
ex.Op = OpNotSimilar
|
||||
ex.Val = node.Val
|
||||
case "contains":
|
||||
ex.Op = OpContains
|
||||
ex.Val = node.Val
|
||||
case "contained_in":
|
||||
ex.Op = OpContainedIn
|
||||
ex.Val = node.Val
|
||||
case "has_key":
|
||||
ex.Op = OpHasKey
|
||||
ex.Val = node.Val
|
||||
case "has_key_any":
|
||||
ex.Op = OpHasKeyAny
|
||||
ex.Val = node.Val
|
||||
case "has_key_all":
|
||||
ex.Op = OpHasKeyAll
|
||||
ex.Val = node.Val
|
||||
case "is_null":
|
||||
ex.Op = OpIsNull
|
||||
ex.Val = node.Val
|
||||
case "null_eq", "ndis", "not_distinct":
|
||||
ex.Op = OpNotDistinct
|
||||
ex.Val = node.Val
|
||||
case "null_neq", "dis", "distinct":
|
||||
ex.Op = OpDistinct
|
||||
ex.Val = node.Val
|
||||
default:
|
||||
pushChildren(st, node.exp, node)
|
||||
return nil, nil // skip node
|
||||
}
|
||||
|
||||
if ex.Op != OpAnd && ex.Op != OpOr && ex.Op != OpNot {
|
||||
switch node.Type {
|
||||
case NodeStr:
|
||||
ex.Type = ValStr
|
||||
case NodeInt:
|
||||
ex.Type = ValInt
|
||||
case NodeBool:
|
||||
ex.Type = ValBool
|
||||
case NodeFloat:
|
||||
ex.Type = ValFloat
|
||||
case NodeList:
|
||||
ex.Type = ValList
|
||||
case NodeVar:
|
||||
ex.Type = ValVar
|
||||
default:
|
||||
return nil, fmt.Errorf("[Where] valid values include string, int, float, boolean and list: %s", node.Type)
|
||||
}
|
||||
setWhereColName(ex, node)
|
||||
}
|
||||
|
||||
return ex, nil
|
||||
}
|
||||
|
||||
func setListVal(ex *Exp, node *Node) {
|
||||
if len(node.Children) != 0 {
|
||||
switch node.Children[0].Type {
|
||||
case NodeStr:
|
||||
ex.ListType = ValStr
|
||||
case NodeInt:
|
||||
ex.ListType = ValInt
|
||||
case NodeBool:
|
||||
ex.ListType = ValBool
|
||||
case NodeFloat:
|
||||
ex.ListType = ValFloat
|
||||
}
|
||||
}
|
||||
for i := range node.Children {
|
||||
ex.ListVal = append(ex.ListVal, node.Children[i].Val)
|
||||
}
|
||||
}
|
||||
|
||||
func setWhereColName(ex *Exp, node *Node) {
|
||||
var list []string
|
||||
|
||||
for n := node.Parent; n != nil; n = n.Parent {
|
||||
if n.Type != NodeObj {
|
||||
continue
|
||||
}
|
||||
if len(n.Name) != 0 {
|
||||
k := n.Name
|
||||
if k == "and" || k == "or" || k == "not" ||
|
||||
k == "_and" || k == "_or" || k == "_not" {
|
||||
continue
|
||||
}
|
||||
list = append([]string{k}, list...)
|
||||
}
|
||||
}
|
||||
listlen := len(list)
|
||||
|
||||
if listlen == 1 {
|
||||
ex.Col = list[0]
|
||||
} else if listlen > 1 {
|
||||
ex.Col = list[listlen-1]
|
||||
ex.NestedCols = list[:listlen]
|
||||
}
|
||||
}
|
||||
|
||||
func setOrderByColName(ob *OrderBy, node *Node) {
|
||||
var list []string
|
||||
|
||||
for n := node; n != nil; n = n.Parent {
|
||||
if len(n.Name) != 0 {
|
||||
list = append([]string{n.Name}, list...)
|
||||
}
|
||||
}
|
||||
if len(list) != 0 {
|
||||
ob.Col = buildPath(list)
|
||||
}
|
||||
}
|
||||
|
||||
func pushChildren(st *util.Stack, exp *Exp, node *Node) {
|
||||
for i := range node.Children {
|
||||
node.Children[i].exp = exp
|
||||
st.Push(node.Children[i])
|
||||
}
|
||||
}
|
||||
|
||||
func pushChild(st *util.Stack, exp *Exp, node *Node) {
|
||||
node.Children[0].exp = exp
|
||||
st.Push(node.Children[0])
|
||||
}
|
||||
|
||||
func compileFilter(filter []string) (*Exp, bool, error) {
|
||||
var fl *Exp
|
||||
var needsUser bool
|
||||
|
||||
com := &Compiler{}
|
||||
st := util.NewStack()
|
||||
|
||||
if len(filter) == 0 {
|
||||
return &Exp{Op: OpNop, doFree: false}, false, nil
|
||||
}
|
||||
|
||||
for i := range filter {
|
||||
if filter[i] == "false" {
|
||||
return &Exp{Op: OpFalse, doFree: false}, false, nil
|
||||
}
|
||||
|
||||
node, err := ParseArgValue(filter[i])
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
f, nu, err := com.compileArgNode(st, node, false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if nu {
|
||||
needsUser = true
|
||||
}
|
||||
|
||||
// TODO: Invalid table names in nested where causes fail silently
|
||||
// returning a nil 'f' this needs to be fixed
|
||||
|
||||
// TODO: Invalid where clauses such as missing op (eg. eq) also fail silently
|
||||
|
||||
if fl == nil {
|
||||
fl = f
|
||||
} else {
|
||||
fl = &Exp{Op: OpAnd, Children: []*Exp{fl, f}, doFree: false}
|
||||
}
|
||||
}
|
||||
return fl, needsUser, nil
|
||||
}
|
||||
|
||||
func buildPath(a []string) string {
|
||||
switch len(a) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return a[0]
|
||||
}
|
||||
|
||||
n := len(a) - 1
|
||||
for i := 0; i < len(a); i++ {
|
||||
n += len(a[i])
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(n)
|
||||
b.WriteString(a[0])
|
||||
for _, s := range a[1:] {
|
||||
b.WriteRune('.')
|
||||
b.WriteString(s)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (t ExpOp) String() string {
|
||||
var v string
|
||||
|
||||
switch t {
|
||||
case OpNop:
|
||||
v = "op-nop"
|
||||
case OpAnd:
|
||||
v = "op-and"
|
||||
case OpOr:
|
||||
v = "op-or"
|
||||
case OpNot:
|
||||
v = "op-not"
|
||||
case OpEquals:
|
||||
v = "op-equals"
|
||||
case OpNotEquals:
|
||||
v = "op-not-equals"
|
||||
case OpGreaterOrEquals:
|
||||
v = "op-greater-or-equals"
|
||||
case OpLesserOrEquals:
|
||||
v = "op-lesser-or-equals"
|
||||
case OpGreaterThan:
|
||||
v = "op-greater-than"
|
||||
case OpLesserThan:
|
||||
v = "op-lesser-than"
|
||||
case OpIn:
|
||||
v = "op-in"
|
||||
case OpNotIn:
|
||||
v = "op-not-in"
|
||||
case OpLike:
|
||||
v = "op-like"
|
||||
case OpNotLike:
|
||||
v = "op-not-like"
|
||||
case OpILike:
|
||||
v = "op-i-like"
|
||||
case OpNotILike:
|
||||
v = "op-not-i-like"
|
||||
case OpSimilar:
|
||||
v = "op-similar"
|
||||
case OpNotSimilar:
|
||||
v = "op-not-similar"
|
||||
case OpContains:
|
||||
v = "op-contains"
|
||||
case OpContainedIn:
|
||||
v = "op-contained-in"
|
||||
case OpHasKey:
|
||||
v = "op-has-key"
|
||||
case OpHasKeyAny:
|
||||
v = "op-has-key-any"
|
||||
case OpHasKeyAll:
|
||||
v = "op-has-key-all"
|
||||
case OpIsNull:
|
||||
v = "op-is-null"
|
||||
case OpEqID:
|
||||
v = "op-eq-id"
|
||||
case OpTsQuery:
|
||||
v = "op-ts-query"
|
||||
}
|
||||
return fmt.Sprintf("<%s>", v)
|
||||
}
|
||||
|
||||
func FreeExp(ex *Exp) {
|
||||
if ex.doFree {
|
||||
expPool.Put(ex)
|
||||
}
|
||||
}
|
||||
|
||||
func argErr(name, ty string) error {
|
||||
return fmt.Errorf("value for argument '%s' must be a %s", name, ty)
|
||||
}
|
47
core/internal/qcode/stack.go
Normal file
47
core/internal/qcode/stack.go
Normal file
@ -0,0 +1,47 @@
|
||||
package qcode
|
||||
|
||||
type Stack struct {
|
||||
stA [20]int32
|
||||
st []int32
|
||||
top int
|
||||
}
|
||||
|
||||
// Create a new Stack
|
||||
func NewStack() *Stack {
|
||||
s := &Stack{top: -1}
|
||||
s.st = s.stA[:0]
|
||||
return s
|
||||
}
|
||||
|
||||
// Return the number of items in the Stack
|
||||
func (s *Stack) Len() int {
|
||||
return (s.top + 1)
|
||||
}
|
||||
|
||||
// View the top item on the Stack
|
||||
func (s *Stack) Peek() int32 {
|
||||
if s.top == -1 {
|
||||
return -1
|
||||
}
|
||||
return s.st[s.top]
|
||||
}
|
||||
|
||||
// Pop the top item of the Stack and return it
|
||||
func (s *Stack) Pop() int32 {
|
||||
if s.top == -1 {
|
||||
return -1
|
||||
}
|
||||
|
||||
s.top--
|
||||
return s.st[(s.top + 1)]
|
||||
}
|
||||
|
||||
// Push a value onto the top of the Stack
|
||||
func (s *Stack) Push(value int32) {
|
||||
s.top++
|
||||
if len(s.st) <= s.top {
|
||||
s.st = append(s.st, value)
|
||||
} else {
|
||||
s.st[s.top] = value
|
||||
}
|
||||
}
|
42
core/internal/qcode/utils.go
Normal file
42
core/internal/qcode/utils.go
Normal file
@ -0,0 +1,42 @@
|
||||
package qcode
|
||||
|
||||
func GetQType(gql string) QType {
|
||||
for i := range gql {
|
||||
b := gql[i]
|
||||
if b == '{' {
|
||||
return QTQuery
|
||||
}
|
||||
if al(b) {
|
||||
switch b {
|
||||
case 'm', 'M':
|
||||
return QTMutation
|
||||
case 'q', 'Q':
|
||||
return QTQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func al(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
func (qt QType) String() string {
|
||||
switch qt {
|
||||
case QTQuery:
|
||||
return "query"
|
||||
case QTMutation:
|
||||
return "mutation"
|
||||
case QTInsert:
|
||||
return "insert"
|
||||
case QTUpdate:
|
||||
return "update"
|
||||
case QTDelete:
|
||||
return "delete"
|
||||
case QTUpsert:
|
||||
return "upsert"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
Reference in New Issue
Block a user