Skip to content

Commit 004ee3d

Browse files
author
Miguel Molina
authored
Merge pull request #55 from erizocosmico/feature/array-ops
Implement array operators
2 parents f497df9 + a9cd307 commit 004ee3d

File tree

3 files changed

+237
-40
lines changed

3 files changed

+237
-40
lines changed

operators.go

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package kallax
22

33
import (
4+
"database/sql/driver"
45
"fmt"
56

7+
"github.com/src-d/go-kallax/types"
8+
69
"github.com/Masterminds/squirrel"
710
)
811

912
// Condition represents a condition of filtering in a query.
1013
type Condition func(Schema) squirrel.Sqlizer
1114

12-
type not struct {
13-
cond squirrel.Sqlizer
14-
}
15-
1615
// Eq returns a condition that will be true when `col` is equal to `value`.
1716
func Eq(col SchemaField, value interface{}) Condition {
1817
return func(schema Schema) squirrel.Sqlizer {
@@ -94,14 +93,100 @@ func NotIn(col SchemaField, values ...interface{}) Condition {
9493
}
9594
}
9695

97-
func condsToSqlizers(conds []Condition, schema Schema) []squirrel.Sqlizer {
98-
var result = make([]squirrel.Sqlizer, len(conds))
99-
for i, v := range conds {
100-
result[i] = v(schema)
96+
// ArrayEq returns a condition that will be true when `col` is equal to an
97+
// array with the given elements.
98+
func ArrayEq(col SchemaField, values ...interface{}) Condition {
99+
return func(schema Schema) squirrel.Sqlizer {
100+
return &colOp{col.QualifiedName(schema), "=", types.Slice(values)}
101+
}
102+
}
103+
104+
// ArrayNotEq returns a condition that will be true when `col` is not equal to
105+
// an array with the given elements.
106+
func ArrayNotEq(col SchemaField, values ...interface{}) Condition {
107+
return func(schema Schema) squirrel.Sqlizer {
108+
return &colOp{col.QualifiedName(schema), "<>", types.Slice(values)}
109+
}
110+
}
111+
112+
// ArrayLt returns a condition that will be true when all elements in `col`
113+
// are lower or equal than their counterparts in the given values, and one of
114+
// the elements at least is lower than its counterpart in the given values.
115+
// For example: for a col with values [1,2,2] and values [1,2,3], it will be
116+
// true.
117+
func ArrayLt(col SchemaField, values ...interface{}) Condition {
118+
return func(schema Schema) squirrel.Sqlizer {
119+
return &colOp{col.QualifiedName(schema), "<", types.Slice(values)}
120+
}
121+
}
122+
123+
// ArrayGt returns a condition that will be true when all elements in `col`
124+
// are greater or equal than their counterparts in the given values, and one of
125+
// the elements at least is greater than its counterpart in the given values.
126+
// For example: for a col with values [1,2,3] and values [1,2,2], it will be
127+
// true.
128+
func ArrayGt(col SchemaField, values ...interface{}) Condition {
129+
return func(schema Schema) squirrel.Sqlizer {
130+
return &colOp{col.QualifiedName(schema), ">", types.Slice(values)}
131+
}
132+
}
133+
134+
// ArrayLtOrEq returns a condition that will be true when all elements in `col`
135+
// are lower or equal than their counterparts in the given values.
136+
// For example: for a col with values [1,2,2] and values [1,2,2], it will be
137+
// true.
138+
func ArrayLtOrEq(col SchemaField, values ...interface{}) Condition {
139+
return func(schema Schema) squirrel.Sqlizer {
140+
return &colOp{col.QualifiedName(schema), "<=", types.Slice(values)}
141+
}
142+
}
143+
144+
// ArrayGtOrEq returns a condition that will be true when all elements in `col`
145+
// are greater or equal than their counterparts in the given values.
146+
// For example: for a col with values [1,2,2] and values [1,2,2], it will be
147+
// true.
148+
func ArrayGtOrEq(col SchemaField, values ...interface{}) Condition {
149+
return func(schema Schema) squirrel.Sqlizer {
150+
return &colOp{col.QualifiedName(schema), ">=", types.Slice(values)}
151+
}
152+
}
153+
154+
// ArrayContains returns a condition that will be true when `col` contains all the
155+
// given values.
156+
func ArrayContains(col SchemaField, values ...interface{}) Condition {
157+
return func(schema Schema) squirrel.Sqlizer {
158+
return &colOp{col.QualifiedName(schema), "@>", types.Slice(values)}
159+
}
160+
}
161+
162+
// ArrayContainedBy returns a condition that will be true when `col` has all
163+
// its elements present in the given values.
164+
func ArrayContainedBy(col SchemaField, values ...interface{}) Condition {
165+
return func(schema Schema) squirrel.Sqlizer {
166+
return &colOp{col.QualifiedName(schema), "<@", types.Slice(values)}
101167
}
102-
return result
103168
}
104169

170+
// ArrayOverlap returns a condition that will be true when `col` has elements
171+
// in common with an array formed by the given values.
172+
func ArrayOverlap(col SchemaField, values ...interface{}) Condition {
173+
return func(schema Schema) squirrel.Sqlizer {
174+
return &colOp{col.QualifiedName(schema), "&&", types.Slice(values)}
175+
}
176+
}
177+
178+
type (
179+
not struct {
180+
cond squirrel.Sqlizer
181+
}
182+
183+
colOp struct {
184+
col string
185+
op string
186+
valuer driver.Valuer
187+
}
188+
)
189+
105190
func (n not) ToSql() (string, []interface{}, error) {
106191
sql, args, err := n.cond.ToSql()
107192
if err != nil {
@@ -110,3 +195,15 @@ func (n not) ToSql() (string, []interface{}, error) {
110195

111196
return fmt.Sprintf("NOT (%s)", sql), args, err
112197
}
198+
199+
func (o colOp) ToSql() (string, []interface{}, error) {
200+
return fmt.Sprintf("%s %s ?", o.col, o.op), []interface{}{o.valuer}, nil
201+
}
202+
203+
func condsToSqlizers(conds []Condition, schema Schema) []squirrel.Sqlizer {
204+
var result = make([]squirrel.Sqlizer, len(conds))
205+
for i, v := range conds {
206+
result[i] = v(schema)
207+
}
208+
return result
209+
}

operators_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package kallax
2+
3+
import (
4+
"database/sql"
5+
"testing"
6+
7+
"github.com/src-d/go-kallax/types"
8+
"github.com/stretchr/testify/suite"
9+
)
10+
11+
type OpsSuite struct {
12+
suite.Suite
13+
db *sql.DB
14+
store *Store
15+
}
16+
17+
func (s *OpsSuite) SetupTest() {
18+
var err error
19+
s.db, err = openTestDB()
20+
s.Nil(err)
21+
_, err = s.db.Exec(`CREATE TABLE model (
22+
id uuid PRIMARY KEY,
23+
name varchar(255) not null,
24+
email varchar(255) not null,
25+
age int not null
26+
)`)
27+
s.Nil(err)
28+
_, err = s.db.Exec(`CREATE TABLE slices (
29+
id uuid PRIMARY KEY,
30+
elems bigint[]
31+
)`)
32+
s.Nil(err)
33+
s.store = NewStore(s.db)
34+
}
35+
36+
func (s *OpsSuite) TearDownTest() {
37+
_, err := s.db.Exec("DROP TABLE slices")
38+
s.NoError(err)
39+
40+
_, err = s.db.Exec("DROP TABLE model")
41+
s.NoError(err)
42+
}
43+
44+
func (s *OpsSuite) TestOperators() {
45+
cases := []struct {
46+
name string
47+
cond Condition
48+
count int64
49+
}{
50+
{"Eq", Eq(f("name"), "Joe"), 1},
51+
{"Gt", Gt(f("age"), 1), 2},
52+
{"Lt", Lt(f("age"), 2), 1},
53+
{"Neq", Neq(f("name"), "Joe"), 2},
54+
{"GtOrEq", GtOrEq(f("age"), 2), 2},
55+
{"LtOrEq", LtOrEq(f("age"), 3), 3},
56+
{"Not", Not(Eq(f("name"), "Joe")), 2},
57+
{"And", And(Neq(f("name"), "Joe"), Gt(f("age"), 1)), 2},
58+
{"Or", Or(Neq(f("name"), "Joe"), Eq(f("age"), 1)), 3},
59+
{"In", In(f("name"), "Joe", "Jane"), 2},
60+
{"NotIn", NotIn(f("name"), "Joe", "Jane"), 1},
61+
}
62+
63+
s.Nil(s.store.Insert(ModelSchema, newModel("Joe", "", 1)))
64+
s.Nil(s.store.Insert(ModelSchema, newModel("Jane", "", 2)))
65+
s.Nil(s.store.Insert(ModelSchema, newModel("Anna", "", 2)))
66+
67+
for _, c := range cases {
68+
q := NewBaseQuery(ModelSchema)
69+
q.Where(c.cond)
70+
71+
s.Equal(s.store.MustCount(q), c.count, c.name)
72+
}
73+
}
74+
75+
func (s *OpsSuite) TestArrayOperators() {
76+
f := f("elems")
77+
78+
cases := []struct {
79+
name string
80+
cond Condition
81+
ok bool
82+
}{
83+
{"ArrayEq", ArrayEq(f, 1, 2, 3), true},
84+
{"ArrayEq fail", ArrayEq(f, 1, 2, 2), false},
85+
{"ArrayNotEq", ArrayNotEq(f, 1, 2, 2), true},
86+
{"ArrayNotEq fail", ArrayNotEq(f, 1, 2, 3), false},
87+
{"ArrayGt", ArrayGt(f, 1, 2, 2), true},
88+
{"ArrayGt all eq", ArrayGt(f, 1, 2, 3), false},
89+
{"ArrayGt some lt", ArrayGt(f, 1, 3, 1), false},
90+
{"ArrayLt", ArrayLt(f, 1, 2, 4), true},
91+
{"ArrayLt all eq", ArrayLt(f, 1, 2, 3), false},
92+
{"ArrayLt some gt", ArrayLt(f, 1, 1, 4), false},
93+
{"ArrayGtOrEq", ArrayGtOrEq(f, 1, 2, 2), true},
94+
{"ArrayGtOrEq all eq", ArrayGtOrEq(f, 1, 2, 3), true},
95+
{"ArrayGtOrEq some lt", ArrayGtOrEq(f, 1, 3, 1), false},
96+
{"ArrayLtOrEq", ArrayLtOrEq(f, 1, 2, 4), true},
97+
{"ArrayLtOrEq all eq", ArrayLtOrEq(f, 1, 2, 3), true},
98+
{"ArrayLtOrEq some gt", ArrayLtOrEq(f, 1, 1, 4), false},
99+
{"ArrayContains", ArrayContains(f, 1, 2), true},
100+
{"ArrayContains fail", ArrayContains(f, 5, 6), false},
101+
{"ArrayContainedBy", ArrayContainedBy(f, 1, 2, 3, 5, 6), true},
102+
{"ArrayContainedBy fail", ArrayContainedBy(f, 1, 2, 5, 6), false},
103+
{"ArrayOverlap", ArrayOverlap(f, 5, 1, 7), true},
104+
{"ArrayOverlap fail", ArrayOverlap(f, 6, 7, 8, 9), false},
105+
}
106+
107+
_, err := s.db.Exec("INSERT INTO slices (id,elems) VALUES ($1, $2)", NewID(), types.Slice([]int64{1, 2, 3}))
108+
s.NoError(err)
109+
110+
for _, c := range cases {
111+
q := NewBaseQuery(SlicesSchema)
112+
q.Where(c.cond)
113+
cnt, err := s.store.Count(q)
114+
s.NoError(err, c.name)
115+
s.Equal(c.ok, cnt > 0, "success: %s", c.name)
116+
}
117+
}
118+
119+
func TestOperators(t *testing.T) {
120+
suite.Run(t, new(OpsSuite))
121+
}
122+
123+
var SlicesSchema = &BaseSchema{
124+
alias: "_sl",
125+
table: "slices",
126+
id: f("id"),
127+
columns: []SchemaField{
128+
f("id"),
129+
f("elems"),
130+
},
131+
}

store_test.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -385,37 +385,6 @@ func (s *StoreSuite) TestFind_1toNMultiple() {
385385
s.Equal(100, i)
386386
}
387387

388-
func (s *StoreSuite) TestOperators() {
389-
cases := []struct {
390-
name string
391-
cond Condition
392-
count int64
393-
}{
394-
{"Eq", Eq(f("name"), "Joe"), 1},
395-
{"Gt", Gt(f("age"), 1), 2},
396-
{"Lt", Lt(f("age"), 2), 1},
397-
{"Neq", Neq(f("name"), "Joe"), 2},
398-
{"GtOrEq", GtOrEq(f("age"), 2), 2},
399-
{"LtOrEq", LtOrEq(f("age"), 3), 3},
400-
{"Not", Not(Eq(f("name"), "Joe")), 2},
401-
{"And", And(Neq(f("name"), "Joe"), Gt(f("age"), 1)), 2},
402-
{"Or", Or(Neq(f("name"), "Joe"), Eq(f("age"), 1)), 3},
403-
{"In", In(f("name"), "Joe", "Jane"), 2},
404-
{"NotIn", NotIn(f("name"), "Joe", "Jane"), 1},
405-
}
406-
407-
s.Nil(s.store.Insert(ModelSchema, newModel("Joe", "", 1)))
408-
s.Nil(s.store.Insert(ModelSchema, newModel("Jane", "", 2)))
409-
s.Nil(s.store.Insert(ModelSchema, newModel("Anna", "", 2)))
410-
411-
for _, c := range cases {
412-
q := NewBaseQuery(ModelSchema)
413-
q.Where(c.cond)
414-
415-
s.Equal(s.store.MustCount(q), c.count, c.name)
416-
}
417-
}
418-
419388
func (s *StoreSuite) assertModel(m *model) {
420389
var result model
421390
err := s.db.QueryRow("SELECT id, name, email, age FROM model WHERE id = $1", m.ID).

0 commit comments

Comments
 (0)