chore: add mercury

This commit is contained in:
xuu
2024-01-22 16:00:58 -07:00
parent eb63312542
commit d4e021386b
47 changed files with 6456 additions and 152 deletions

300
rsql/squirrel/sqlizer.go Normal file
View 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
}

View 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 }