Add an internalFilter wrapping struct for filter.Filter
The struct simplifies filter manipulation when performing analysis and optimization.
This commit is contained in:
144
pkg/dal/filter.go
Normal file
144
pkg/dal/filter.go
Normal 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
268
pkg/dal/filter_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user