DB service (#132)

This commit is contained in:
Janos Dobronszki
2021-06-02 15:29:52 +01:00
committed by GitHub
parent 4d12230338
commit 7157db9aaa
15 changed files with 1389 additions and 4 deletions

108
db/handler/db.go Normal file
View File

@@ -0,0 +1,108 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
db "github.com/micro/services/db/proto"
gorm2 "github.com/micro/services/pkg/gorm"
"gorm.io/datatypes"
)
type Record struct {
ID string
Data datatypes.JSON `json:"data"`
}
type Db struct {
gorm2.Helper
}
// Call is a single request handler called via client.Call or the generated client code
func (e *Db) Create(ctx context.Context, req *db.CreateRequest, rsp *db.CreateResponse) error {
db, err := e.GetDBConn(ctx)
if err != nil {
return err
}
m := map[string]interface{}{}
err = json.Unmarshal([]byte(req.Record), &m)
if err != nil {
return err
}
if _, ok := m["ID"].(string); !ok {
m["ID"] = uuid.New().String()
}
bs, _ := json.Marshal(m)
return db.Table(req.Table).Create(Record{
ID: m["ID"].(string),
Data: bs,
}).Error
}
func (e *Db) Update(ctx context.Context, req *db.UpdateRequest, rsp *db.UpdateResponse) error {
return nil
}
func (e *Db) Read(ctx context.Context, req *db.ReadRequest, rsp *db.ReadResponse) error {
recs := []Record{}
queries, err := Parse(req.Query)
if err != nil {
return err
}
db, err := e.GetDBConn(ctx)
if err != nil {
return err
}
db = db.Table(req.Table)
for _, query := range queries {
typ := "text"
switch query.Value.(type) {
case int64:
typ = "int"
case bool:
typ = "boolean"
}
op := ""
switch query.Op {
case itemEquals:
op = "="
case itemGreaterThan:
op = ">"
case itemGreaterThanEquals:
op = ">="
case itemLessThan:
op = "<"
case itemLessThanEquals:
op = "<="
case itemNotEquals:
op = "!="
}
db = db.Where(fmt.Sprintf("(data ->> '%v')::%v %v ?", query.Field, typ, op), query.Value)
}
err = db.Find(&recs).Error
if err != nil {
return err
}
ret := []map[string]interface{}{}
for _, rec := range recs {
m, err := rec.Data.MarshalJSON()
if err != nil {
return err
}
ma := map[string]interface{}{}
json.Unmarshal(m, &ma)
ma["ID"] = rec.ID
ret = append(ret, ma)
}
bs, _ := json.Marshal(ret)
rsp.Records = string(bs)
return nil
}
func (e *Db) Delete(ctx context.Context, req *db.DeleteRequest, rsp *db.DeleteResponse) error {
return nil
}

134
db/handler/parse.go Normal file
View File

@@ -0,0 +1,134 @@
package handler
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/crufter/lexer"
)
var quoteEscape = fmt.Sprint(0x10FFFF)
const (
itemIgnore = iota
// nouns
itemAnd
itemInt
itemFieldName
itemString
itemBoolTrue
itemBoolFalse
// ops
itemEquals
itemNotEquals
itemLessThan
itemGreaterThan
itemLessThanEquals
itemGreaterThanEquals
)
var opToString = map[int]string{
itemEquals: "==",
itemNotEquals: "!=",
itemLessThan: "<",
itemGreaterThan: ">",
itemLessThanEquals: "<=",
itemGreaterThanEquals: ">=",
}
var expressions = []lexer.TokenExpr{
{`[ ]+`, itemIgnore}, // Whitespace
{`==`, itemEquals},
{`!=`, itemNotEquals},
{`false`, itemBoolFalse},
{`true`, itemBoolTrue},
{`and`, itemAnd},
{`<=`, itemLessThanEquals},
{`>=`, itemGreaterThanEquals},
{`<`, itemLessThan},
{`>`, itemGreaterThan},
{`[0-9]+`, itemInt},
{`"(?:[^"\\]|\\.)*"`, itemString},
{`[\<\>\!\=\+\-\|\&\*\/A-Za-z][A-Za-z0-9_]*`, itemFieldName},
}
type Query struct {
Field string
Op int
Value interface{}
}
func Parse(q string) ([]Query, error) {
if strings.Contains(q, quoteEscape) {
return nil, errors.New("query contains illegal max rune")
}
q = strings.Replace(q, `""`, quoteEscape, -1)
tokens, err := lexer.Lex(q, expressions)
if err != nil {
return nil, err
}
queries := []Query{}
current := Query{}
for i, token := range tokens {
// and tokens should trigger a query
// save and reset
if token.Typ == itemAnd {
queries = append(queries, current)
current = Query{}
continue
}
// is an op
if token.Typ >= itemEquals {
current.Op = token.Typ
continue
}
// is a value
switch token.Typ {
case itemFieldName:
current.Field = token.Text
case itemString:
switch current.Op {
case itemEquals, itemNotEquals:
default:
return nil, fmt.Errorf("operator '%v' can't be used with strings", opToString[token.Typ])
}
if len(token.Text) < 2 {
return nil, fmt.Errorf("string literal too short: '%v'", token.Text)
}
current.Value = strings.Replace(token.Text[1:len(token.Text)-1], quoteEscape, `"`, -1)
case itemBoolTrue:
switch current.Op {
case itemEquals, itemNotEquals:
default:
return nil, fmt.Errorf("operator '%v' can't be used with bools", opToString[token.Typ])
}
current.Value = true
case itemBoolFalse:
switch current.Op {
case itemEquals, itemNotEquals:
default:
return nil, fmt.Errorf("operator '%v' can't be used with bools", opToString[token.Typ])
}
current.Value = false
case itemInt:
num, err := strconv.ParseInt(token.Text, 10, 64)
if err != nil {
return nil, err
}
current.Value = num
}
// if we are at last position, save last query
if i == len(tokens)-1 {
queries = append(queries, current)
}
}
return queries, nil
}

141
db/handler/parse_test.go Normal file
View File

@@ -0,0 +1,141 @@
package handler
import (
"fmt"
"reflect"
"testing"
"github.com/crufter/lexer"
)
func TestLexing(t *testing.T) {
tokens, err := lexer.Lex("a == 12", expressions)
if err != nil {
t.Fatal(err)
}
if len(tokens) != 3 {
t.Fatal(tokens)
}
if tokens[0].Typ != itemFieldName || tokens[1].Typ != itemEquals || tokens[2].Typ != itemInt {
t.Fatal(tokens)
}
tokens, err = lexer.Lex(`a == 12 and name != "nandos"`, expressions)
if tokens[0].Typ != itemFieldName ||
tokens[1].Typ != itemEquals ||
tokens[2].Typ != itemInt ||
tokens[3].Typ != itemAnd ||
tokens[4].Typ != itemFieldName ||
tokens[5].Typ != itemNotEquals ||
tokens[6].Typ != itemString {
t.Fatal(tokens)
}
}
type tCase struct {
Q string
E []Query
Err error
}
func TestParsing(t *testing.T) {
tCases := []tCase{
tCase{
Q: `a == 12 and name != "nandos"`,
E: []Query{
Query{
Field: "a",
Value: int64(12),
Op: itemEquals,
},
Query{
Field: "name",
Value: "nandos",
Op: itemNotEquals,
},
},
},
// test escaping quotes
tCase{
Q: `a == 12 and name != "He said ""yes""!"`,
E: []Query{
Query{
Field: "a",
Value: int64(12),
Op: itemEquals,
},
Query{
Field: "name",
Value: `He said "yes"!`,
Op: itemNotEquals,
},
},
},
tCase{
Q: `a == false and b == true`,
E: []Query{
Query{
Field: "a",
Value: false,
Op: itemEquals,
},
Query{
Field: "b",
Value: true,
Op: itemEquals,
},
},
},
// a < 20
tCase{
Q: `a < 20`,
E: []Query{
Query{
Field: "a",
Value: int64(20),
Op: itemLessThan,
},
},
},
tCase{
Q: `a <= 20`,
E: []Query{
Query{
Field: "a",
Value: int64(20),
Op: itemLessThanEquals,
},
},
},
tCase{
Q: `a > 20`,
E: []Query{
Query{
Field: "a",
Value: int64(20),
Op: itemGreaterThan,
},
},
},
tCase{
Q: `a >= 20`,
E: []Query{
Query{
Field: "a",
Value: int64(20),
Op: itemGreaterThanEquals,
},
},
},
}
for _, tCase := range tCases {
fmt.Println("Parsing", tCase.Q)
qs, err := Parse(tCase.Q)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(qs, tCase.E) {
t.Fatal("Expected", tCase.E, "got", qs)
}
}
}