Skip to content

Commit f804715

Browse files
Add more integration tests (#2)
Tests all operators, JSONB and error cases against actual Postgres. Co-authored-by: Koen Bollen <[email protected]>
1 parent debe1f2 commit f804715

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-0
lines changed

integration/postgres_test.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package integration
22

33
import (
44
"context"
5+
"errors"
56
"reflect"
7+
"strings"
68
"testing"
79

810
"github.com/lib/pq"
@@ -217,3 +219,265 @@ func TestIntegration_InAny_PGX(t *testing.T) {
217219
t.Fatalf("expected [3, 4, 5, 6, 7, 8, 9, 10], got %v", ids)
218220
}
219221
}
222+
223+
func TestIntegration_BasicOperators(t *testing.T) {
224+
db := setupPQ(t)
225+
226+
createPlayersTable(t, db)
227+
228+
tests := []struct {
229+
name string
230+
input string
231+
expectedPlayers []int
232+
expectedError error
233+
}{
234+
{
235+
`$gt`,
236+
`{"level": {"$gt": 50}}`,
237+
[]int{6, 7, 8, 9, 10},
238+
nil,
239+
},
240+
{
241+
`$gte`,
242+
`{"level": {"$gte": 50}}`,
243+
[]int{5, 6, 7, 8, 9, 10},
244+
nil,
245+
},
246+
{
247+
`$lt`,
248+
`{"level": {"$lt": 50}}`,
249+
[]int{1, 2, 3, 4},
250+
nil,
251+
},
252+
{
253+
`$lte`,
254+
`{"level": {"$lte": 50}}`,
255+
[]int{1, 2, 3, 4, 5},
256+
nil,
257+
},
258+
{
259+
`$eq`,
260+
`{"name": "Alice"}`,
261+
[]int{1},
262+
nil,
263+
},
264+
{
265+
`$ne`,
266+
`{"name": {"$eq": "Alice"}}`,
267+
[]int{1},
268+
nil,
269+
},
270+
{
271+
`$ne`,
272+
`{"name": {"$ne": "Alice"}}`,
273+
[]int{2, 3, 4, 5, 6, 7, 8, 9, 10},
274+
nil,
275+
},
276+
{
277+
`$regex`,
278+
`{"name": {"$regex": "a.k$"}}`,
279+
[]int{6, 8, 10},
280+
nil,
281+
},
282+
{
283+
`unknown column`,
284+
`{"foobar": "admin"}`,
285+
nil,
286+
errors.New("pq: column \"foobar\" does not exist"),
287+
},
288+
{
289+
`invalid value`,
290+
`{"level": "town1"}`, // Level is an integer column, but the value is a string.
291+
nil,
292+
errors.New("pq: invalid input syntax for type integer: \"town1\""),
293+
},
294+
}
295+
296+
for _, tt := range tests {
297+
t.Run(tt.name, func(t *testing.T) {
298+
c := filter.NewConverter(filter.WithArrayDriver(pq.Array))
299+
where, values, err := c.Convert([]byte(tt.input))
300+
if err != nil {
301+
t.Fatal(err)
302+
}
303+
304+
rows, err := db.Query(`
305+
SELECT id
306+
FROM players
307+
WHERE `+where+`;
308+
`, values...)
309+
if err != nil {
310+
if tt.expectedError == nil {
311+
t.Fatalf("unexpected error: %v", err)
312+
} else if !strings.Contains(err.Error(), tt.expectedError.Error()) {
313+
t.Fatalf("expected error %q, got %q", tt.expectedError, err)
314+
}
315+
return
316+
}
317+
defer rows.Close()
318+
players := []int{}
319+
for rows.Next() {
320+
var id int
321+
if err := rows.Scan(&id); err != nil {
322+
t.Fatal(err)
323+
}
324+
players = append(players, id)
325+
}
326+
327+
if !reflect.DeepEqual(players, tt.expectedPlayers) {
328+
t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where)
329+
}
330+
})
331+
}
332+
333+
for op := range filter.BasicOperatorMap {
334+
found := false
335+
for _, tt := range tests {
336+
if strings.Contains(tt.input, op) {
337+
found = true
338+
break
339+
}
340+
}
341+
if !found {
342+
t.Fatalf("operator %q is not tested", op)
343+
}
344+
}
345+
}
346+
347+
func TestIntegration_NestedJSONB(t *testing.T) {
348+
db := setupPQ(t)
349+
350+
createPlayersTable(t, db)
351+
352+
tests := []struct {
353+
name string
354+
input string
355+
expectedPlayers []int
356+
}{
357+
{
358+
"jsonb equals",
359+
`{"guild_id": 20}`,
360+
[]int{1, 2},
361+
},
362+
{
363+
"jsonb regex",
364+
`{"pet": {"$regex": "^.{3}$"}}`,
365+
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
366+
},
367+
{
368+
"excemption column",
369+
`{"name": "Alice"}`,
370+
[]int{1},
371+
},
372+
{
373+
"unknown column",
374+
`{"foobar": "admin"}`,
375+
[]int{}, // Will always default to the jsonb column and return no results since it doesn't exist.
376+
},
377+
{
378+
"invalid value",
379+
`{"guild_id": "dragon_slayers"}`, // Guild ID only contains integer values in the test data.
380+
[]int{},
381+
},
382+
}
383+
384+
for _, tt := range tests {
385+
t.Run(tt.name, func(t *testing.T) {
386+
c := filter.NewConverter(filter.WithArrayDriver(pq.Array), filter.WithNestedJSONB("metadata", "name", "level", "class"))
387+
where, values, err := c.Convert([]byte(tt.input))
388+
if err != nil {
389+
t.Fatal(err)
390+
}
391+
392+
rows, err := db.Query(`
393+
SELECT id
394+
FROM players
395+
WHERE `+where+`;
396+
`, values...)
397+
if err != nil {
398+
t.Fatal(err)
399+
}
400+
defer rows.Close()
401+
players := []int{}
402+
for rows.Next() {
403+
var id int
404+
if err := rows.Scan(&id); err != nil {
405+
t.Fatal(err)
406+
}
407+
players = append(players, id)
408+
}
409+
410+
if !reflect.DeepEqual(players, tt.expectedPlayers) {
411+
t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where)
412+
}
413+
})
414+
}
415+
}
416+
417+
func TestIntegration_Logic(t *testing.T) {
418+
db := setupPQ(t)
419+
420+
createPlayersTable(t, db)
421+
422+
tests := []struct {
423+
name string
424+
input string
425+
expectedPlayers []int
426+
}{
427+
{
428+
"basic or",
429+
`{"$or": [{"level": {"$gt": 50}}, {"pet": "dog"}]}`,
430+
[]int{1, 3, 5, 6, 7, 8, 9, 10},
431+
},
432+
{
433+
// (mages and (ends with E or ends with K)) or (dog owners and (guild in (50, 20)))
434+
"complex triple nested",
435+
`{"$or": [
436+
{"$and": [
437+
{"class": "mage"},
438+
{"$or": [
439+
{"name": {"$regex": "e$"}},
440+
{"name": {"$regex": "k$"}}
441+
]}
442+
]},
443+
{"$and": [
444+
{"pet": "dog"},
445+
{"guild_id": {"$in": [50, 20]}}
446+
]}
447+
]}`,
448+
[]int{1, 5, 7, 8},
449+
},
450+
}
451+
452+
for _, tt := range tests {
453+
t.Run(tt.name, func(t *testing.T) {
454+
c := filter.NewConverter(filter.WithArrayDriver(pq.Array), filter.WithNestedJSONB("metadata", "name", "level", "class"))
455+
where, values, err := c.Convert([]byte(tt.input))
456+
if err != nil {
457+
t.Fatal(err)
458+
}
459+
460+
rows, err := db.Query(`
461+
SELECT id
462+
FROM players
463+
WHERE `+where+`;
464+
`, values...)
465+
if err != nil {
466+
t.Fatal(err)
467+
}
468+
defer rows.Close()
469+
players := []int{}
470+
for rows.Next() {
471+
var id int
472+
if err := rows.Scan(&id); err != nil {
473+
t.Fatal(err)
474+
}
475+
players = append(players, id)
476+
}
477+
478+
if !reflect.DeepEqual(players, tt.expectedPlayers) {
479+
t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where)
480+
}
481+
})
482+
}
483+
}

integration/setup_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,36 @@ func setupDatabase(t *testing.T, connect func(string) error) {
103103
}
104104
})
105105
}
106+
107+
// createPlayersTable create a players table with 10 players.
108+
func createPlayersTable(t *testing.T, db *sql.DB) {
109+
t.Helper()
110+
111+
if _, err := db.Exec(`
112+
CREATE TABLE players (
113+
"id" serial PRIMARY KEY,
114+
"name" text,
115+
"metadata" jsonb,
116+
"level" int,
117+
"class" text
118+
);
119+
`); err != nil {
120+
t.Fatal(err)
121+
}
122+
if _, err := db.Exec(`
123+
INSERT INTO players ("id", "name", "metadata", "level", "class")
124+
VALUES
125+
(1, 'Alice', '{"guild_id": 20, "pet": "dog"}', 10, 'warrior'),
126+
(2, 'Bob', '{"guild_id": 20, "pet": "cat"}', 20, 'mage'),
127+
(3, 'Charlie', '{"guild_id": 30, "pet": "dog"}', 30, 'rogue'),
128+
(4, 'David', '{"guild_id": 30, "pet": "cat"}', 40, 'warrior'),
129+
(5, 'Eve', '{"guild_id": 40, "pet": "dog"}', 50, 'mage'),
130+
(6, 'Frank', '{"guild_id": 40, "pet": "cat"}', 60, 'rogue'),
131+
(7, 'Grace', '{"guild_id": 50, "pet": "dog"}', 70, 'warrior'),
132+
(8, 'Hank', '{"guild_id": 50, "pet": "cat"}', 80, 'mage'),
133+
(9, 'Ivy', '{"guild_id": 60, "pet": "dog"}', 90, 'rogue'),
134+
(10, 'Jack', '{"guild_id": 60, "pet": "cat"}', 100, 'warrior')
135+
`); err != nil {
136+
t.Fatal(err)
137+
}
138+
}

0 commit comments

Comments
 (0)