package dal import ( "testing" "time" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/stretchr/testify/require" ) func TestCompareValues(t *testing.T) { n := time.Now() m := n.Add(time.Second) tcc := []struct { name string a any b any out int }{ { name: "two ints; eq", a: 10, b: 10, out: 0, }, { name: "two ints; lt", a: 9, b: 10, out: -1, }, { name: "two ints; gt", a: 10, b: 9, out: 1, }, { name: "two ints; a nil", a: nil, b: 10, out: -1, }, { name: "two ints; b nil", a: 10, b: nil, out: 1, }, { name: "two strings; eq", a: "aa", b: "aa", out: 0, }, { name: "two strings; lt", a: "a", b: "aa", out: -1, }, { name: "two strings; gt", a: "aa", b: "a", out: 1, }, { name: "two strings; a nil", a: nil, b: "aa", out: -1, }, { name: "two strings; b nil", a: "aa", b: nil, out: 1, }, { name: "two uints; eq", a: uint(10), b: uint(10), out: 0, }, { name: "two uints; lt", a: uint(9), b: uint(10), out: -1, }, { name: "two uints; gt", a: uint(10), b: uint(9), out: 1, }, { name: "two uints; a nil", a: nil, b: uint(10), out: -1, }, { name: "two uints; b nil", a: uint(10), b: nil, out: 1, }, { name: "two floats; eq", a: float64(10), b: float64(10), out: 0, }, { name: "two floats; lt", a: float64(9), b: float64(10), out: -1, }, { name: "two floats; gt", a: float64(10), b: float64(9), out: 1, }, { name: "two floats; a nil", a: nil, b: float64(10), out: -1, }, { name: "two floats; b nil", a: float64(10), b: nil, out: 1, }, { name: "two times; eq", a: n, b: n, out: 0, }, { name: "two times; lt", a: n, b: m, out: -1, }, { name: "two times; gt", a: m, b: n, out: 1, }, { name: "two times; a nil", a: nil, b: n, out: -1, }, { name: "two times; b nil", a: n, b: nil, out: 1, }, { name: "two nils", a: nil, b: nil, out: 0, }, } for _, c := range tcc { t.Run(c.name, func(t *testing.T) { require.Equal(t, c.out, compareValues(c.a, c.b)) }) } } func TestCompareGetters(t *testing.T) { tcc := []struct { name string a ValueGetter b ValueGetter attr string out int }{{ name: "single eq", a: simpleRow{"a": 10}, b: simpleRow{"a": 10}, attr: "a", out: 0, }, { name: "single lt", a: simpleRow{"a": 9}, b: simpleRow{"a": 10}, attr: "a", out: -1, }, { name: "single gt", a: simpleRow{"a": 10}, b: simpleRow{"a": 9}, attr: "a", out: 1, }, { name: "multi eq both empty", a: &Row{}, b: &Row{}, attr: "a", out: 0, }, { name: "multi eq same values", a: (&Row{}).WithValue("a", 0, 1), b: (&Row{}).WithValue("a", 0, 1), attr: "a", out: 0, }, { name: "multi lt a less items", a: (&Row{}), b: (&Row{}).WithValue("a", 0, 1), attr: "a", out: -1, }, { name: "multi lt a item less", a: (&Row{}).WithValue("a", 0, 0), b: (&Row{}).WithValue("a", 0, 1), attr: "a", out: -1, }, { name: "multi gt a more items", a: (&Row{}).WithValue("a", 0, 1), b: (&Row{}), attr: "a", out: 1, }, { name: "multi gt a item more", a: (&Row{}).WithValue("a", 0, 1), b: (&Row{}).WithValue("a", 0, 0), attr: "a", out: 1, }} for _, c := range tcc { t.Run(c.name, func(t *testing.T) { require.Equal(t, c.out, compareGetters(c.a, c.b, c.a.CountValues(), c.b.CountValues(), c.attr)) }) } } func TestConstraintsToExpression(t *testing.T) { tcc := []struct { name string cc map[string][]any out []string }{{ name: "one constraint, one value", cc: map[string][]any{ "k1": {"v1"}, }, out: []string{`k1 == 'v1'`}, }, { name: "one constraint, multiple values", cc: map[string][]any{ "k1": {"v1", 10, true}, }, out: []string{`k1 == 'v1' || k1 == 10 || k1 == true`}, }, { name: "multiple constraints, multiple values", cc: map[string][]any{ "k1": {"v1", 10, true}, "k2": {"v2", 42, false}, }, out: []string{ `(k1 == 'v1' || k1 == 10 || k1 == true)`, `(k2 == 'v2' || k2 == 42 || k2 == false)`, }, }} for _, c := range tcc { t.Run(c.name, func(t *testing.T) { // Do it like so since map order is not defined and the test would be flaky got := constraintsToExpression(c.cc) for _, o := range c.out { require.Contains(t, got, o) } }) } } func TestStateConstraintsToExpression(t *testing.T) { tcc := []struct { name string cc map[string]filter.State out []string }{{ name: "exclude these ones", cc: map[string]filter.State{ "k1": filter.StateExcluded, "k2": filter.StateExcluded, }, out: []string{`k1 == null`, `k2 == null`}, }, { name: "only these ones", cc: map[string]filter.State{ "k1": filter.StateExclusive, "k2": filter.StateExclusive, }, out: []string{`k1 != null`, `k2 != null`}, }, { name: "both", cc: map[string]filter.State{ "k1": filter.StateExclusive, "k2": filter.StateExclusive, }, out: []string{}, }, { name: "mix and match", cc: map[string]filter.State{ "k1": filter.StateExcluded, "k2": filter.StateInclusive, "k3": filter.StateExclusive, }, out: []string{ `k1 == null`, `k3 != null`, }, }} for _, c := range tcc { t.Run(c.name, func(t *testing.T) { // Do it like so since map order is not defined and the test would be flaky got := stateConstraintsToExpression(c.cc) for _, o := range c.out { require.Contains(t, got, o) } }) } } func TestMergeRows(t *testing.T) { tcc := []struct { name string a *Row b *Row mapping []AttributeMapping out *Row }{{ name: "full merge; no mapping", a: (&Row{}).WithValue("attr1", 0, 10).WithValue("attr2", 0, "hi").WithValue("attr2", 1, "hello"), b: (&Row{}).WithValue("attr3", 0, true).WithValue("attr4", 0, "ee").WithValue("attr4", 1, 25), out: (&Row{}).WithValue("attr1", 0, 10).WithValue("attr2", 0, "hi").WithValue("attr2", 1, "hello").WithValue("attr3", 0, true).WithValue("attr4", 0, "ee").WithValue("attr4", 1, 25), }, { name: "full merge; no mapping; collision", a: (&Row{}).WithValue("attr1", 0, 10).WithValue("attr2", 0, "hi").WithValue("attr2", 1, "hello"), b: (&Row{}).WithValue("attr2", 0, true).WithValue("attr3", 0, "ee").WithValue("attr3", 1, 25), out: (&Row{}).WithValue("attr1", 0, 10).WithValue("attr2", 0, true).WithValue("attr3", 0, "ee").WithValue("attr3", 1, 25), }, { name: "mapped merge", a: (&Row{}).WithValue("attr1", 0, 10).WithValue("attr2", 0, "hi").WithValue("attr2", 1, "hello"), b: (&Row{}).WithValue("attr3", 0, true).WithValue("attr4", 0, "ee").WithValue("attr4", 1, 25), out: (&Row{}).WithValue("a", 0, 10).WithValue("b", 0, "hi").WithValue("b", 1, "hello").WithValue("c", 0, true).WithValue("d", 0, "ee").WithValue("d", 1, 25), mapping: saToMapping([]simpleAttribute{{ ident: "a", source: "attr1", }, { ident: "b", source: "attr2", }, { ident: "c", source: "attr3", }, { ident: "d", source: "attr4", }}...), }, { name: "mapped merge with conflicts", a: (&Row{}).WithValue("attr1", 0, 10).WithValue("attr2", 0, "hi").WithValue("attr2", 1, "hello"), b: (&Row{}).WithValue("attr3", 0, true).WithValue("attr4", 0, "ee").WithValue("attr4", 1, 25), out: (&Row{}).WithValue("a", 0, 10).WithValue("b", 0, true).WithValue("c", 0, "ee").WithValue("c", 1, 25), mapping: saToMapping([]simpleAttribute{{ ident: "a", source: "attr1", }, { ident: "b", source: "attr2", }, { ident: "b", source: "attr3", }, { ident: "c", source: "attr4", }}...), }} for _, c := range tcc { t.Run(c.name, func(t *testing.T) { out := &Row{} err := mergeRows(c.mapping, out, c.a, c.b) require.NoError(t, err) require.Equal(t, c.out, out) }) } } func TestRowComparator(t *testing.T) { // @todo add some more extreme cases tcc := []struct { name string a ValueGetter b ValueGetter ss filter.SortExprSet less bool }{ // Simple one col cases { name: "single column simple asc less", a: simpleRow{"a": 10}, b: simpleRow{"a": 20}, ss: filter.SortExprSet{{Column: "a", Descending: false}}, less: true, }, { name: "single column simple asc more", a: simpleRow{"a": 20}, b: simpleRow{"a": 10}, ss: filter.SortExprSet{{Column: "a", Descending: false}}, less: false, }, { name: "single column simple asc equal", a: simpleRow{"a": 10}, b: simpleRow{"a": 10}, ss: filter.SortExprSet{{Column: "a", Descending: false}}, less: false, }, { name: "single column simple desc less", a: simpleRow{"a": 20}, b: simpleRow{"a": 10}, ss: filter.SortExprSet{{Column: "a", Descending: true}}, less: true, }, { name: "single column simple desc more", a: simpleRow{"a": 10}, b: simpleRow{"a": 20}, ss: filter.SortExprSet{{Column: "a", Descending: true}}, less: false, }, { name: "single column simple desc equal", a: simpleRow{"a": 10}, b: simpleRow{"a": 10}, ss: filter.SortExprSet{{Column: "a", Descending: true}}, less: false, }, // basic 2 col cases { name: "two column asc less first priority", a: simpleRow{"a": 10, "b": 100}, b: simpleRow{"a": 20, "b": 1}, ss: filter.SortExprSet{{Column: "a", Descending: false}, {Column: "b", Descending: false}}, less: true, }, { name: "two column asc less first equal", a: simpleRow{"a": 10, "b": 1}, b: simpleRow{"a": 10, "b": 2}, ss: filter.SortExprSet{{Column: "a", Descending: false}, {Column: "b", Descending: false}}, less: true, }, { name: "two column asc equal", a: simpleRow{"a": 10, "b": 1}, b: simpleRow{"a": 10, "b": 1}, ss: filter.SortExprSet{{Column: "a", Descending: false}, {Column: "b", Descending: false}}, less: false, }, { name: "two column desc less first priority", a: simpleRow{"a": 20, "b": 1}, b: simpleRow{"a": 10, "b": 100}, ss: filter.SortExprSet{{Column: "a", Descending: true}, {Column: "b", Descending: true}}, less: true, }, { name: "two column desc less first equal", a: simpleRow{"a": 10, "b": 2}, b: simpleRow{"a": 10, "b": 1}, ss: filter.SortExprSet{{Column: "a", Descending: true}, {Column: "b", Descending: true}}, less: true, }, { name: "two column desc equal", a: simpleRow{"a": 10, "b": 1}, b: simpleRow{"a": 10, "b": 1}, ss: filter.SortExprSet{{Column: "a", Descending: true}, {Column: "b", Descending: true}}, less: false, }, } for _, c := range tcc { t.Run(c.name, func(t *testing.T) { less := makeRowComparator(c.ss...)(c.a, c.b) require.Equal(t, c.less, less) }) } } func TestRowResetting(t *testing.T) { r := &Row{} gv := func(ident string, pos uint) any { v, err := r.GetValue(ident, pos) require.NoError(t, err) return v } r.SetValue("a", 0, 1) r.SetValue("a", 0, 2) r.SetValue("a", 1, 1) require.Equal(t, 2, gv("a", 0)) require.Equal(t, 1, gv("a", 1)) require.Equal(t, uint(2), r.counters["a"]) r.Reset() require.Equal(t, uint(0), r.counters["a"]) r.SetValue("a", 0, 3) r.SetValue("b", 0, 4) require.Equal(t, 3, gv("a", 0)) require.Equal(t, uint(1), r.counters["a"]) require.Equal(t, 4, gv("b", 0)) require.Equal(t, uint(1), r.counters["b"]) }