initial v0.8 pre-release

This commit is contained in:
Gani Georgiev
2022-10-30 10:28:14 +02:00
parent 9cbb2e750e
commit 90dba45d7c
388 changed files with 21580 additions and 13603 deletions
+38 -32
View File
@@ -89,52 +89,39 @@ func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldRes
return nil, fmt.Errorf("Invalid right operand %q - %v.", expr.Right.Literal, rErr)
}
// merge both operands parameters (if any)
params := dbx.Params{}
for k, v := range lParams {
params[k] = v
}
for k, v := range rParams {
params[k] = v
}
switch expr.Op {
case fexpr.SignEq:
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') = COALESCE(%s, '')", lName, rName), params), nil
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') = COALESCE(%s, '')", lName, rName), mergeParams(lParams, rParams)), nil
case fexpr.SignNeq:
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", lName, rName), params), nil
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", lName, rName), mergeParams(lParams, rParams)), nil
case fexpr.SignLike:
// both sides are columns and therefore wrap the right side with "%" for contains like behavior
if len(params) == 0 {
return dbx.NewExp(fmt.Sprintf("%s LIKE ('%%' || %s || '%%')", lName, rName), params), nil
// the right side is a column and therefor wrap it with "%" for contains like behavior
if len(rParams) == 0 {
return dbx.NewExp(fmt.Sprintf("%s LIKE ('%%' || %s || '%%')", lName, rName), lParams), nil
}
// normalize operands and switch sides if the left operand is a number or text
if len(lParams) > 0 {
return dbx.NewExp(fmt.Sprintf("%s LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil
}
return dbx.NewExp(fmt.Sprintf("%s LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil
return dbx.NewExp(fmt.Sprintf("%s LIKE %s", lName, rName), mergeParams(lParams, wrapLikeParams(rParams))), nil
case fexpr.SignNlike:
// both sides are columns and therefore wrap the right side with "%" for not-contains like behavior
if len(params) == 0 {
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE ('%%' || %s || '%%')", lName, rName), params), nil
// the right side is a column and therefor wrap it with "%" for not-contains like behavior
if len(rParams) == 0 {
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE ('%%' || %s || '%%')", lName, rName), lParams), nil
}
// normalize operands and switch sides if the left operand is a number or text
if len(lParams) > 0 {
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil
// normalize operands and switch sides if the left operand is a number/text, but the right one is a column
// (usually this shouldn't be needed, but it's kept for backward compatibility)
if len(lParams) > 0 && len(rParams) == 0 {
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", rName, lName), wrapLikeParams(lParams)), nil
}
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", lName, rName), mergeParams(lParams, wrapLikeParams(rParams))), nil
case fexpr.SignLt:
return dbx.NewExp(fmt.Sprintf("%s < %s", lName, rName), params), nil
return dbx.NewExp(fmt.Sprintf("%s < %s", lName, rName), mergeParams(lParams, rParams)), nil
case fexpr.SignLte:
return dbx.NewExp(fmt.Sprintf("%s <= %s", lName, rName), params), nil
return dbx.NewExp(fmt.Sprintf("%s <= %s", lName, rName), mergeParams(lParams, rParams)), nil
case fexpr.SignGt:
return dbx.NewExp(fmt.Sprintf("%s > %s", lName, rName), params), nil
return dbx.NewExp(fmt.Sprintf("%s > %s", lName, rName), mergeParams(lParams, rParams)), nil
case fexpr.SignGte:
return dbx.NewExp(fmt.Sprintf("%s >= %s", lName, rName), params), nil
return dbx.NewExp(fmt.Sprintf("%s >= %s", lName, rName), mergeParams(lParams, rParams)), nil
}
return nil, fmt.Errorf("Unknown expression operator %q", expr.Op)
@@ -190,12 +177,31 @@ func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver)
return "", nil, errors.New("Unresolvable token type.")
}
func (f FilterData) normalizeLikeParams(params dbx.Params) dbx.Params {
// mergeParams returns new dbx.Params where each provided params item
// is merged in the order they are specified.
func mergeParams(params ...dbx.Params) dbx.Params {
result := dbx.Params{}
for _, p := range params {
for k, v := range p {
result[k] = v
}
}
return result
}
// wrapLikeParams wraps each provided param value string with `%`
// if the string doesn't contains the `%` char (including its escape sequence).
func wrapLikeParams(params dbx.Params) dbx.Params {
result := dbx.Params{}
for k, v := range params {
vStr := cast.ToString(v)
if !strings.Contains(vStr, "%") {
for i := 0; i < len(dbx.DefaultLikeEscape); i += 2 {
vStr = strings.ReplaceAll(vStr, dbx.DefaultLikeEscape[i], dbx.DefaultLikeEscape[i+1])
}
vStr = "%" + vStr + "%"
}
result[k] = vStr
+21 -5
View File
@@ -38,8 +38,16 @@ func TestFilterDataBuildExpr(t *testing.T) {
regexp.QuoteMeta("[[test1]] LIKE ('%' || [[test2]] || '%')") +
"$",
},
// reversed like with text
// like with right column operand
{"'lorem' ~ test1", false,
"^" +
regexp.QuoteMeta("{:") +
".+" +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%')") +
"$",
},
// like with left column operand and text as right operand
{"test1 ~ 'lorem'", false,
"^" +
regexp.QuoteMeta("[[test1]] LIKE {:") +
".+" +
@@ -52,8 +60,16 @@ func TestFilterDataBuildExpr(t *testing.T) {
regexp.QuoteMeta("[[test1]] NOT LIKE ('%' || [[test2]] || '%')") +
"$",
},
// reversed not like with text
// not like with right column operand
{"'lorem' !~ test1", false,
"^" +
regexp.QuoteMeta("{:") +
".+" +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test1]] || '%')") +
"$",
},
// like with left column operand and text as right operand
{"test1 !~ 'lorem'", false,
"^" +
regexp.QuoteMeta("[[test1]] NOT LIKE {:") +
".+" +
@@ -97,11 +113,11 @@ func TestFilterDataBuildExpr(t *testing.T) {
".+" +
regexp.QuoteMeta("}) OR ([[test2]] NOT LIKE {:") +
".+" +
regexp.QuoteMeta("}))) AND ([[test1]] LIKE {:") +
regexp.QuoteMeta("}))) AND ({:") +
".+" +
regexp.QuoteMeta("})) AND ([[test2]] NOT LIKE {:") +
regexp.QuoteMeta("} LIKE ('%' || [[test1]] || '%'))) AND ({:") +
".+" +
regexp.QuoteMeta("})) AND ([[test3]] > {:") +
regexp.QuoteMeta("} NOT LIKE ('%' || [[test2]] || '%'))) AND ([[test3]] > {:") +
".+" +
regexp.QuoteMeta("})) AND ([[test3]] >= {:") +
".+" +
+14 -16
View File
@@ -5,6 +5,7 @@ import (
"math"
"net/url"
"strconv"
"strings"
"github.com/pocketbase/dbx"
)
@@ -13,7 +14,7 @@ import (
const DefaultPerPage int = 30
// MaxPerPage specifies the maximum allowed search result items returned in a single page.
const MaxPerPage int = 400
const MaxPerPage int = 500
// url search query params
const (
@@ -38,7 +39,6 @@ type Provider struct {
query *dbx.SelectQuery
page int
perPage int
countColumn string
sort []SortField
filter []FilterData
}
@@ -53,7 +53,7 @@ type Provider struct {
//
// result, err := search.NewProvider(fieldResolver).
// Query(baseQuery).
// ParseAndExec("page=2&filter=id>0&sort=-name", &models)
// ParseAndExec("page=2&filter=id>0&sort=-email", &models)
func NewProvider(fieldResolver FieldResolver) *Provider {
return &Provider{
fieldResolver: fieldResolver,
@@ -70,13 +70,6 @@ func (s *Provider) Query(query *dbx.SelectQuery) *Provider {
return s
}
// CountColumn specifies an optional distinct column to use in the
// SELECT COUNT query.
func (s *Provider) CountColumn(countColumn string) *Provider {
s.countColumn = countColumn
return s
}
// Page sets the `page` field of the current search provider.
//
// Normalization on the `page` value is done during `Exec()`.
@@ -170,7 +163,7 @@ func (s *Provider) Exec(items any) (*Result, error) {
// clone provider's query
modelsQuery := *s.query
// apply filters
// build filters
for _, f := range s.filter {
expr, err := f.BuildExpr(s.fieldResolver)
if err != nil {
@@ -197,14 +190,19 @@ func (s *Provider) Exec(items any) (*Result, error) {
return nil, err
}
queryInfo := modelsQuery.Info()
// count
var totalCount int64
countQuery := modelsQuery
countQuery.Distinct(false).Select("COUNT(*)").OrderBy() // unset ORDER BY statements
if s.countColumn != "" {
countQuery.Select("COUNT(DISTINCT(" + s.countColumn + "))")
var baseTable string
if len(queryInfo.From) > 0 {
baseTable = queryInfo.From[0]
}
if err := countQuery.Row(&totalCount); err != nil {
countQuery := modelsQuery
rawCountQuery := countQuery.Select(strings.Join([]string{baseTable, "id"}, ".")).OrderBy().Build().SQL()
wrappedCountQuery := queryInfo.Builder.NewQuery("SELECT COUNT(*) FROM (" + rawCountQuery + ")")
wrappedCountQuery.Bind(countQuery.Build().Params())
if err := wrappedCountQuery.Row(&totalCount); err != nil {
return nil, err
}
+12 -44
View File
@@ -60,15 +60,6 @@ func TestProviderPerPage(t *testing.T) {
}
}
func TestProviderCountColumn(t *testing.T) {
r := &testFieldResolver{}
p := NewProvider(r).CountColumn("test")
if p.countColumn != "test" {
t.Fatalf("Expected distinct count column %v, got %v", "test", p.countColumn)
}
}
func TestProviderSort(t *testing.T) {
initialSort := []SortField{{"test1", SortAsc}, {"test2", SortAsc}}
r := &testFieldResolver{}
@@ -223,7 +214,6 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
perPage int
sort []SortField
filter []FilterData
countColumn string
expectError bool
expectResult string
expectQueries []string
@@ -234,11 +224,10 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
10,
[]SortField{},
[]FilterData{},
"",
false,
`{"page":1,"perPage":10,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE NOT (`test1` IS NULL))",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10",
},
},
@@ -248,11 +237,10 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
0, // fallback to default
[]SortField{},
[]FilterData{},
"",
false,
`{"page":1,"perPage":30,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE NOT (`test1` IS NULL))",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30",
},
},
@@ -262,7 +250,6 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
10,
[]SortField{{"unknown", SortAsc}},
[]FilterData{},
"",
true,
"",
nil,
@@ -273,7 +260,6 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
10,
[]SortField{},
[]FilterData{"test2 = 'test2.1'", "invalid"},
"",
true,
"",
nil,
@@ -284,12 +270,11 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
5555, // will be limited by MaxPerPage
[]SortField{{"test2", SortDesc}},
[]FilterData{"test2 != null", "test1 >= 2"},
"",
false,
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2)",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 400",
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2))",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 500",
},
},
// valid sort and filter fields (zero results)
@@ -298,11 +283,10 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
10,
[]SortField{{"test3", SortAsc}},
[]FilterData{"test3 != ''"},
"",
false,
`{"page":1,"perPage":10,"totalItems":0,"totalPages":0,"items":[]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', ''))",
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')))",
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
},
},
@@ -312,25 +296,10 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
1,
[]SortField{},
[]FilterData{},
"",
false,
`{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
},
},
// distinct count column
{
3,
1,
[]SortField{},
[]FilterData{},
"test.test1",
false,
`{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(DISTINCT(test.test1)) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE NOT (`test1` IS NULL))",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
},
},
@@ -345,8 +314,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
Page(s.page).
PerPage(s.perPage).
Sort(s.sort).
Filter(s.filter).
CountColumn(s.countColumn)
Filter(s.filter)
result, err := p.Exec(&[]testTableStruct{})
@@ -376,7 +344,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
for _, q := range testDB.CalledQueries {
if !list.ExistInSliceWithRegex(q, s.expectQueries) {
t.Errorf("(%d) Didn't expect query \n%v", i, q)
t.Errorf("(%d) Didn't expect query \n%v in \n%v", i, q, testDB.CalledQueries)
}
}
}
@@ -439,7 +407,7 @@ func TestProviderParseAndExec(t *testing.T) {
{
"page=3&perPage=9999&filter=test1>1&sort=-test2,test3",
false,
`{"page":1,"perPage":400,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
`{"page":1,"perPage":500,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
},
}
@@ -504,9 +472,9 @@ func createTestDB() (*testDB, error) {
}
db := testDB{DB: dbx.NewFromDB(sqlDB, "sqlite")}
db.CreateTable("test", map[string]string{"test1": "int default 0", "test2": "text default ''", "test3": "text default ''"}).Execute()
db.Insert("test", dbx.Params{"test1": 1, "test2": "test2.1"}).Execute()
db.Insert("test", dbx.Params{"test1": 2, "test2": "test2.2"}).Execute()
db.CreateTable("test", map[string]string{"id": "int default 0", "test1": "int default 0", "test2": "text default ''", "test3": "text default ''"}).Execute()
db.Insert("test", dbx.Params{"id": 1, "test1": 1, "test2": "test2.1"}).Execute()
db.Insert("test", dbx.Params{"id": 2, "test1": 2, "test2": "test2.2"}).Execute()
db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
db.CalledQueries = append(db.CalledQueries, sql)
}