3
0

Add an internalFilter wrapping struct for filter.Filter

The struct simplifies filter manipulation when performing
analysis and optimization.
This commit is contained in:
Tomaž Jerman
2022-08-21 16:53:59 +02:00
parent 61051e998e
commit 40dbbae730
2 changed files with 412 additions and 0 deletions

144
pkg/dal/filter.go Normal file
View File

@@ -0,0 +1,144 @@
package dal
import (
"fmt"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/ql"
)
type (
// internalFilter is a wrap to simplify interactions for cases where we might
// want to interact with the received filters; i.e. when optimizing pipeline steps
internalFilter struct {
constraints map[string][]any
stateConstraints map[string]filter.State
expression string
expParsed *ql.ASTNode
orderBy filter.SortExprSet
limit uint
cursor *filter.PagingCursor
}
)
func (f internalFilter) Constraints() map[string][]any { return f.constraints }
func (f internalFilter) StateConstraints() map[string]filter.State { return f.stateConstraints }
func (f internalFilter) Expression() string { return f.expression }
func (f internalFilter) OrderBy() filter.SortExprSet { return f.orderBy }
func (f internalFilter) Limit() uint { return f.limit }
func (f internalFilter) Cursor() *filter.PagingCursor { return f.cursor }
// toInternalFilter converts filter.Filter to internalFilter for easier manipulation
//
// nil filters are returned as zeroed-out internalFilters.
func toInternalFilter(f filter.Filter) (out internalFilter, err error) {
if f == nil {
return
}
// Base
out = internalFilter{
constraints: f.Constraints(),
stateConstraints: f.StateConstraints(),
expression: f.Expression(),
orderBy: f.OrderBy(),
limit: f.Limit(),
cursor: f.Cursor(),
}
// Parse expression foe later use
if out.expression != "" {
out.expParsed, err = ql.NewParser().Parse(out.expression)
if err != nil {
return
}
}
return
}
// mergeFilters returns a new filter based on a overwritten by values from b
func (a internalFilter) mergeFilters(b internalFilter) (c internalFilter) {
c = a
// expression
if b.expParsed != nil {
if c.expParsed == nil {
c.expParsed = b.expParsed
c.expression = b.expression
} else {
parsedA := a.expParsed
rawA := a.expression
if parsedA.Ref != "group" {
parsedA = &ql.ASTNode{
Ref: "group",
Args: []*ql.ASTNode{
parsedA,
},
}
rawA = fmt.Sprintf("(%s)", rawA)
}
parsedB := b.expParsed
rawB := b.expression
if parsedB.Ref != "group" {
parsedB = &ql.ASTNode{
Ref: "group",
Args: []*ql.ASTNode{
parsedB,
},
}
rawB = fmt.Sprintf("(%s)", rawB)
}
c.expParsed = &ql.ASTNode{
Ref: "and",
Args: ql.ASTNodeSet{parsedA, parsedB},
}
c.expression = fmt.Sprintf("%s && %s", rawA, rawB)
}
}
// constraints
if b.constraints != nil {
if c.constraints == nil {
c.constraints = make(map[string][]any)
}
for k, v := range b.constraints {
c.constraints[k] = append(c.constraints[k], v...)
}
}
// state constraints
if b.stateConstraints != nil {
if c.stateConstraints == nil {
c.stateConstraints = make(map[string]filter.State)
}
for k, v := range b.stateConstraints {
c.stateConstraints[k] = v
}
}
// order by
if b.orderBy != nil {
c.orderBy = append(c.orderBy, b.orderBy...)
}
// cursor
// always use the latest paging cursor
c.cursor = b.cursor
return
}
// empty reports if the filter is completely empty or not
func (f internalFilter) empty() bool {
return f.constraints == nil &&
f.stateConstraints == nil &&
f.expression == "" &&
f.orderBy == nil &&
f.limit == 0 &&
f.cursor == nil
}

268
pkg/dal/filter_test.go Normal file
View File

@@ -0,0 +1,268 @@
package dal
import (
"testing"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/stretchr/testify/require"
)
func TestInternalFilterMergeFilters_expression(t *testing.T) {
t.Run("b no expr", func(t *testing.T) {
a, err := toInternalFilter(internalFilter{expression: "a == b"})
require.NoError(t, err)
b, err := toInternalFilter(internalFilter{expression: ""})
require.NoError(t, err)
c := a.mergeFilters(b)
require.Equal(t, "a == b", c.expression)
require.Equal(t, "eq(a, b)", c.expParsed.String())
})
t.Run("a no expr", func(t *testing.T) {
a, err := toInternalFilter(internalFilter{expression: ""})
require.NoError(t, err)
b, err := toInternalFilter(internalFilter{expression: "a == b"})
require.NoError(t, err)
c := a.mergeFilters(b)
require.Equal(t, "a == b", c.expression)
require.Equal(t, "eq(a, b)", c.expParsed.String())
})
t.Run("both missing", func(t *testing.T) {
a, err := toInternalFilter(internalFilter{expression: ""})
require.NoError(t, err)
b, err := toInternalFilter(internalFilter{expression: ""})
require.NoError(t, err)
c := a.mergeFilters(b)
require.Equal(t, "", c.expression)
require.Equal(t, "<nil>", c.expParsed.String())
})
t.Run("combine", func(t *testing.T) {
a, err := toInternalFilter(internalFilter{expression: "a == b"})
require.NoError(t, err)
b, err := toInternalFilter(internalFilter{expression: "c == d"})
require.NoError(t, err)
c := a.mergeFilters(b)
require.Equal(t, "(a == b) && (c == d)", c.expression)
require.Equal(t, "and(group(eq(a, b)), group(eq(c, d)))", c.expParsed.String())
})
}
func TestInternalFilterMergeFilters_constraints(t *testing.T) {
t.Run("b no constr", func(t *testing.T) {
a := internalFilter{constraints: map[string][]any{"a": {"b"}}}
b := internalFilter{constraints: nil}
c := a.mergeFilters(b)
require.Equal(t, map[string][]any{"a": {"b"}}, c.constraints)
})
t.Run("a no constr", func(t *testing.T) {
a := internalFilter{constraints: nil}
b := internalFilter{constraints: map[string][]any{"a": {"b"}}}
c := a.mergeFilters(b)
require.Equal(t, map[string][]any{"a": {"b"}}, c.constraints)
})
t.Run("both missing", func(t *testing.T) {
a := internalFilter{constraints: nil}
b := internalFilter{constraints: nil}
c := a.mergeFilters(b)
// Use this assertion because equal asserts type also
require.Nil(t, c.constraints)
})
t.Run("combine", func(t *testing.T) {
a := internalFilter{constraints: map[string][]any{"a": {"b"}}}
b := internalFilter{constraints: map[string][]any{"a": {"c"}, "d": {"e"}}}
c := a.mergeFilters(b)
require.Equal(t, map[string][]any{"a": {"b", "c"}, "d": {"e"}}, c.constraints)
})
}
func TestInternalFilterMergeFilters_stateConstraints(t *testing.T) {
t.Run("b no constr", func(t *testing.T) {
a := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateInclusive}}
b := internalFilter{stateConstraints: nil}
c := a.mergeFilters(b)
require.Equal(t, map[string]filter.State{"a": filter.StateInclusive}, c.stateConstraints)
})
t.Run("a no constr", func(t *testing.T) {
a := internalFilter{stateConstraints: nil}
b := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateInclusive}}
c := a.mergeFilters(b)
require.Equal(t, map[string]filter.State{"a": filter.StateInclusive}, c.stateConstraints)
})
t.Run("both missing", func(t *testing.T) {
a := internalFilter{stateConstraints: nil}
b := internalFilter{stateConstraints: nil}
c := a.mergeFilters(b)
// Use this assertion because equal asserts type also
require.Nil(t, c.stateConstraints)
})
t.Run("combine", func(t *testing.T) {
a := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateInclusive}}
b := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateExcluded, "d": filter.StateExclusive}}
c := a.mergeFilters(b)
require.Equal(t, map[string]filter.State{"a": filter.StateExcluded, "d": filter.StateExclusive}, c.stateConstraints)
})
}
func TestInternalFilterMergeFilters_order(t *testing.T) {
t.Run("b no order", func(t *testing.T) {
a := internalFilter{orderBy: filter.SortExprSet{{Column: "a"}}}
b := internalFilter{orderBy: nil}
c := a.mergeFilters(b)
require.Equal(t, filter.SortExprSet{{Column: "a"}}, c.orderBy)
})
t.Run("a no order", func(t *testing.T) {
a := internalFilter{orderBy: nil}
b := internalFilter{orderBy: filter.SortExprSet{{Column: "a"}}}
c := a.mergeFilters(b)
require.Equal(t, filter.SortExprSet{{Column: "a"}}, c.orderBy)
})
t.Run("both missing", func(t *testing.T) {
a := internalFilter{orderBy: nil}
b := internalFilter{orderBy: nil}
c := a.mergeFilters(b)
// Use this assertion because equal asserts type also
require.Nil(t, c.orderBy)
})
t.Run("combine", func(t *testing.T) {
a := internalFilter{orderBy: filter.SortExprSet{{Column: "a"}}}
b := internalFilter{orderBy: filter.SortExprSet{{Column: "b"}}}
c := a.mergeFilters(b)
require.Equal(t, filter.SortExprSet{{Column: "a"}, {Column: "b"}}, c.orderBy)
})
}
func TestInternalFilterMergeFilters_cursor(t *testing.T) {
t.Run("b no cursor", func(t *testing.T) {
a := internalFilter{cursor: &filter.PagingCursor{ROrder: true}}
b := internalFilter{orderBy: nil}
c := a.mergeFilters(b)
require.Nil(t, c.cursor)
})
t.Run("a no constr", func(t *testing.T) {
a := internalFilter{orderBy: nil}
b := internalFilter{cursor: &filter.PagingCursor{ROrder: true}}
c := a.mergeFilters(b)
require.Equal(t, &filter.PagingCursor{ROrder: true}, c.cursor)
})
t.Run("both missing", func(t *testing.T) {
a := internalFilter{orderBy: nil}
b := internalFilter{orderBy: nil}
c := a.mergeFilters(b)
// Use this assertion because equal asserts type also
require.Nil(t, c.cursor)
})
t.Run("combine", func(t *testing.T) {
a := internalFilter{cursor: &filter.PagingCursor{ROrder: true}}
b := internalFilter{cursor: &filter.PagingCursor{ROrder: false}}
c := a.mergeFilters(b)
require.Equal(t, &filter.PagingCursor{ROrder: false}, c.cursor)
})
}
func TestInternalFilterEmpty(t *testing.T) {
tcc := []struct {
name string
f internalFilter
want bool
}{
{
name: "completely empty",
f: internalFilter{},
want: true,
},
{
name: "only constraints",
f: internalFilter{
constraints: map[string][]any{
"a": {"b"},
},
},
want: false,
},
{
name: "only state constraints",
f: internalFilter{
stateConstraints: map[string]filter.State{
"a": filter.StateInclusive,
},
},
want: false,
},
{
name: "only expression",
f: internalFilter{
expression: "true",
},
want: false,
},
{
name: "only order",
f: internalFilter{
orderBy: filter.SortExprSet{{Column: "a"}},
},
want: false,
},
{
name: "only cursor",
f: internalFilter{
cursor: &filter.PagingCursor{ROrder: true},
},
want: false,
},
{
name: "all",
f: internalFilter{
constraints: map[string][]any{
"a": {"b"},
},
stateConstraints: map[string]filter.State{
"a": filter.StateInclusive,
},
orderBy: filter.SortExprSet{{Column: "a"}},
cursor: &filter.PagingCursor{ROrder: true},
},
want: false,
},
}
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
got := tc.f.empty()
require.Equal(t, tc.want, got)
})
}
}