chore: add mercury
This commit is contained in:
300
rsql/squirrel/sqlizer.go
Normal file
300
rsql/squirrel/sqlizer.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"go.sour.is/pkg/rsql"
|
||||
)
|
||||
|
||||
type dbInfo interface {
|
||||
Col(string) (string, error)
|
||||
}
|
||||
|
||||
type args map[string]string
|
||||
|
||||
func (d *decoder) mkArgs(a args) args {
|
||||
m := make(args, len(a))
|
||||
for k, v := range a {
|
||||
if k == "limit" || k == "offset" {
|
||||
m[k] = v
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if k, err = d.dbInfo.Col(k); err == nil {
|
||||
m[k] = v
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (a args) Limit() (uint64, bool) {
|
||||
if a == nil {
|
||||
return 0, false
|
||||
}
|
||||
if v, ok := a["limit"]; ok {
|
||||
i, err := strconv.ParseUint(v, 10, 64)
|
||||
return i, err == nil
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
func (a args) Offset() (uint64, bool) {
|
||||
if a == nil {
|
||||
return 0, false
|
||||
}
|
||||
if v, ok := a["offset"]; ok {
|
||||
i, err := strconv.ParseUint(v, 10, 64)
|
||||
return i, err == nil
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
func (a args) Order() []string {
|
||||
var lis []string
|
||||
|
||||
for k, v := range a {
|
||||
if k == "limit" || k == "offset" {
|
||||
continue
|
||||
}
|
||||
lis = append(lis, k+" "+v)
|
||||
}
|
||||
|
||||
return lis
|
||||
}
|
||||
|
||||
func Query(in string, db dbInfo) (squirrel.Sqlizer, args, error) {
|
||||
d := decoder{dbInfo: db}
|
||||
program := rsql.DefaultParse(in)
|
||||
sql, err := d.decode(program)
|
||||
return sql, d.mkArgs(program.Args), err
|
||||
}
|
||||
|
||||
type decoder struct {
|
||||
dbInfo dbInfo
|
||||
}
|
||||
|
||||
func (db *decoder) decode(in *rsql.Program) (squirrel.Sqlizer, error) {
|
||||
switch len(in.Statements) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return db.decodeStatement(in.Statements[0])
|
||||
default:
|
||||
a := squirrel.And{}
|
||||
for _, stmt := range in.Statements {
|
||||
d, err := db.decodeStatement(stmt)
|
||||
if d == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a = append(a, d)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
func (db *decoder) decodeStatement(in rsql.Statement) (squirrel.Sqlizer, error) {
|
||||
switch s := in.(type) {
|
||||
case *rsql.ExpressionStatement:
|
||||
return db.decodeExpression(s.Expression)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (db *decoder) decodeExpression(in rsql.Expression) (squirrel.Sqlizer, error) {
|
||||
switch e := in.(type) {
|
||||
case *rsql.InfixExpression:
|
||||
return db.decodeInfix(e)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (db *decoder) decodeInfix(in *rsql.InfixExpression) (squirrel.Sqlizer, error) {
|
||||
|
||||
switch in.Token.Type {
|
||||
case rsql.TokAND:
|
||||
a := squirrel.And{}
|
||||
left, err := db.decodeExpression(in.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch v := left.(type) {
|
||||
case squirrel.And:
|
||||
for _, el := range v {
|
||||
if el != nil {
|
||||
a = append(a, el)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
if v != nil {
|
||||
a = append(a, v)
|
||||
}
|
||||
}
|
||||
|
||||
right, err := db.decodeExpression(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch v := right.(type) {
|
||||
case squirrel.And:
|
||||
for _, el := range v {
|
||||
if el != nil {
|
||||
a = append(a, el)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
if v != nil {
|
||||
a = append(a, v)
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
case rsql.TokOR:
|
||||
left, err := db.decodeExpression(in.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := db.decodeExpression(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.Or{left, right}, nil
|
||||
case rsql.TokEQ:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.Eq{col: v}, nil
|
||||
case rsql.TokLIKE:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch value := v.(type) {
|
||||
case string:
|
||||
return Like{col, strings.Replace(value, "*", "%", -1)}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("LIKE requires a string value")
|
||||
}
|
||||
|
||||
case rsql.TokNEQ:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.NotEq{col: v}, nil
|
||||
case rsql.TokGT:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.Gt{col: v}, nil
|
||||
case rsql.TokGE:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.GtOrEq{col: v}, nil
|
||||
case rsql.TokLT:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.Lt{col: v}, nil
|
||||
case rsql.TokLE:
|
||||
col, err := db.dbInfo.Col(in.Left.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := db.decodeValue(in.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return squirrel.LtOrEq{col: v}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
func (db *decoder) decodeValue(in rsql.Expression) (interface{}, error) {
|
||||
switch v := in.(type) {
|
||||
case *rsql.Array:
|
||||
var values []interface{}
|
||||
for _, el := range v.Elements {
|
||||
v, err := db.decodeValue(el)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
case *rsql.InfixExpression:
|
||||
return db.decodeInfix(v)
|
||||
case *rsql.Identifier:
|
||||
return v.Value, nil
|
||||
case *rsql.Integer:
|
||||
return v.Value, nil
|
||||
case *rsql.Float:
|
||||
return v.Value, nil
|
||||
case *rsql.String:
|
||||
return v.Value, nil
|
||||
case *rsql.Bool:
|
||||
return v.Value, nil
|
||||
case *rsql.Null:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type Like struct {
|
||||
column string
|
||||
value string
|
||||
}
|
||||
|
||||
func (l Like) ToSql() (sql string, args []interface{}, err error) {
|
||||
sql = fmt.Sprintf("%s LIKE(?)", l.column)
|
||||
args = append(args, l.value)
|
||||
return
|
||||
}
|
||||
141
rsql/squirrel/sqlizer_test.go
Normal file
141
rsql/squirrel/sqlizer_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package squirrel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/pkg/rsql"
|
||||
)
|
||||
|
||||
type testTable struct {
|
||||
Foo string `json:"foo"`
|
||||
Bar string `json:"bar"`
|
||||
Baz string `json:"baz"`
|
||||
Director string `json:"director"`
|
||||
Actor string `json:"actor"`
|
||||
Year string `json:"year"`
|
||||
Genres string `json:"genres"`
|
||||
One string `json:"one"`
|
||||
Two string `json:"two"`
|
||||
Family string `json:"family_name"`
|
||||
}
|
||||
|
||||
func TestQuery(t *testing.T) {
|
||||
d := rsql.GetDbColumns(testTable{})
|
||||
is := is.New(t)
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expect squirrel.Sqlizer
|
||||
expectLimit *uint64
|
||||
expectOffset *uint64
|
||||
expectOrder []string
|
||||
fail bool
|
||||
}{
|
||||
{input: "foo==[1, 2, 3]", expect: squirrel.Eq{"foo": []interface{}{1, 2, 3}}},
|
||||
{input: "foo==1,(bar==2;baz==3)", expect: squirrel.Or{squirrel.Eq{"foo": 1}, squirrel.And{squirrel.Eq{"bar": 2}, squirrel.Eq{"baz": 3}}}},
|
||||
|
||||
{input: "foo==1", expect: squirrel.Eq{"foo": 1}},
|
||||
{input: "foo!=1.1", expect: squirrel.NotEq{"foo": 1.1}},
|
||||
{input: "foo==true", expect: squirrel.Eq{"foo": true}},
|
||||
{input: "foo==null", expect: squirrel.Eq{"foo": nil}},
|
||||
{input: "foo>2", expect: squirrel.Gt{"foo": 2}},
|
||||
{input: "foo>=2.1", expect: squirrel.GtOrEq{"foo": 2.1}},
|
||||
{input: "foo<3", expect: squirrel.Lt{"foo": 3}},
|
||||
{input: "foo<=3.1", expect: squirrel.LtOrEq{"foo": 3.1}},
|
||||
|
||||
{input: "foo=eq=1", expect: squirrel.Eq{"foo": 1}},
|
||||
{input: "foo=neq=1.1", expect: squirrel.NotEq{"foo": 1.1}},
|
||||
{input: "foo=gt=2", expect: squirrel.Gt{"foo": 2}},
|
||||
{input: "foo=ge=2.1", expect: squirrel.GtOrEq{"foo": 2.1}},
|
||||
{input: "foo=lt=3", expect: squirrel.Lt{"foo": 3}},
|
||||
{input: "foo=le=3.1", expect: squirrel.LtOrEq{"foo": 3.1}},
|
||||
|
||||
{input: "foo==1;bar==2", expect: squirrel.And{squirrel.Eq{"foo": 1}, squirrel.Eq{"bar": 2}}},
|
||||
{input: "foo==1,bar==2", expect: squirrel.Or{squirrel.Eq{"foo": 1}, squirrel.Eq{"bar": 2}}},
|
||||
{input: "foo==1,bar==2;baz=3", expect: nil},
|
||||
{
|
||||
input: `director=='name\'s';actor=eq="name\'s";Year=le=2000,Year>=2010;one <= -1.0, two != true`,
|
||||
expect: squirrel.And{
|
||||
squirrel.Eq{"director": "name's"},
|
||||
squirrel.Eq{"actor": "name's"},
|
||||
squirrel.Or{
|
||||
squirrel.LtOrEq{"year": 2000},
|
||||
squirrel.GtOrEq{"year": 2010},
|
||||
},
|
||||
squirrel.Or{
|
||||
squirrel.LtOrEq{"one": -1.0},
|
||||
squirrel.NotEq{"two": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `genres==[sci-fi,action] ; genres==[romance,animated,horror] , director~Que*Tarantino`,
|
||||
expect: squirrel.And{
|
||||
squirrel.Eq{"genres": []interface{}{"sci-fi", "action"}},
|
||||
squirrel.Or{
|
||||
squirrel.Eq{"genres": []interface{}{"romance", "animated", "horror"}},
|
||||
Like{"director", "Que%Tarantino"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{input: "", expect: nil},
|
||||
{input: "family_name==LUNDY", expect: squirrel.Eq{"family_name": "LUNDY"}},
|
||||
{input: "family_name==[1,2,null]", expect: squirrel.Eq{"family_name": []interface{}{1, 2, nil}}},
|
||||
{input: "family_name=LUNDY", expect: nil},
|
||||
{input: "family_name==LUNDY and family_name==SMITH", expect: squirrel.And{squirrel.Eq{"family_name": "LUNDY"}, squirrel.Eq{"family_name": "SMITH"}}},
|
||||
{input: "family_name==LUNDY or family_name==SMITH", expect: squirrel.Or{squirrel.Eq{"family_name": "LUNDY"}, squirrel.Eq{"family_name": "SMITH"}}},
|
||||
{input: "foo==1,family_name=LUNDY;baz==2", expect: nil},
|
||||
{input: "foo ~ bar*", expect: Like{"foo", "bar%"}},
|
||||
{input: "foo ~ [bar*,bin*]", expect: nil, fail: true},
|
||||
{input: "foo==1|limit:10", expect: squirrel.Eq{"foo": 1}, expectLimit: ptr(uint64(10))},
|
||||
{input: "foo==1|offset:2", expect: squirrel.Eq{"foo": 1}, expectOffset: ptr(uint64(2))},
|
||||
{
|
||||
input: "foo>=1|limit:10 offset:2 foo:desc",
|
||||
expect: squirrel.GtOrEq{"foo": 1},
|
||||
expectLimit: ptr(uint64(10)),
|
||||
expectOffset: ptr(uint64(2)),
|
||||
expectOrder: []string{"foo desc"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
q, a, err := Query(tt.input, d)
|
||||
if !tt.fail && err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if q != nil {
|
||||
t.Log(q.ToSql())
|
||||
}
|
||||
|
||||
actual := fmt.Sprintf("%#v", q)
|
||||
expect := fmt.Sprintf("%#v", tt.expect)
|
||||
if expect != actual {
|
||||
t.Errorf("test[%d]: %v\n\tinput and expected are not the same. wanted=%s got=%s", i, tt.input, expect, actual)
|
||||
}
|
||||
|
||||
if limit, ok := a.Limit(); tt.expectLimit != nil {
|
||||
is.True(ok)
|
||||
is.Equal(limit, *tt.expectLimit)
|
||||
} else {
|
||||
is.True(!ok)
|
||||
}
|
||||
|
||||
if offset, ok := a.Offset(); tt.expectOffset != nil {
|
||||
is.True(ok)
|
||||
is.Equal(offset, *tt.expectOffset)
|
||||
} else {
|
||||
is.True(!ok)
|
||||
}
|
||||
|
||||
if order := a.Order(); tt.expectOrder != nil {
|
||||
is.Equal(order, tt.expectOrder)
|
||||
} else {
|
||||
is.Equal(len(order), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](t T) *T { return &t }
|
||||
Reference in New Issue
Block a user