diff --git a/pkg/dal/filter.go b/pkg/dal/filter.go new file mode 100644 index 000000000..545a62e45 --- /dev/null +++ b/pkg/dal/filter.go @@ -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 +} diff --git a/pkg/dal/filter_test.go b/pkg/dal/filter_test.go new file mode 100644 index 000000000..7f91fdbd1 --- /dev/null +++ b/pkg/dal/filter_test.go @@ -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, "", 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) + }) + } +}