diff --git a/integration/postgres_test.go b/integration/postgres_test.go index 5c8b1b4..2c0c6d1 100644 --- a/integration/postgres_test.go +++ b/integration/postgres_test.go @@ -2,7 +2,9 @@ package integration import ( "context" + "errors" "reflect" + "strings" "testing" "github.com/lib/pq" @@ -217,3 +219,265 @@ func TestIntegration_InAny_PGX(t *testing.T) { t.Fatalf("expected [3, 4, 5, 6, 7, 8, 9, 10], got %v", ids) } } + +func TestIntegration_BasicOperators(t *testing.T) { + db := setupPQ(t) + + createPlayersTable(t, db) + + tests := []struct { + name string + input string + expectedPlayers []int + expectedError error + }{ + { + `$gt`, + `{"level": {"$gt": 50}}`, + []int{6, 7, 8, 9, 10}, + nil, + }, + { + `$gte`, + `{"level": {"$gte": 50}}`, + []int{5, 6, 7, 8, 9, 10}, + nil, + }, + { + `$lt`, + `{"level": {"$lt": 50}}`, + []int{1, 2, 3, 4}, + nil, + }, + { + `$lte`, + `{"level": {"$lte": 50}}`, + []int{1, 2, 3, 4, 5}, + nil, + }, + { + `$eq`, + `{"name": "Alice"}`, + []int{1}, + nil, + }, + { + `$ne`, + `{"name": {"$eq": "Alice"}}`, + []int{1}, + nil, + }, + { + `$ne`, + `{"name": {"$ne": "Alice"}}`, + []int{2, 3, 4, 5, 6, 7, 8, 9, 10}, + nil, + }, + { + `$regex`, + `{"name": {"$regex": "a.k$"}}`, + []int{6, 8, 10}, + nil, + }, + { + `unknown column`, + `{"foobar": "admin"}`, + nil, + errors.New("pq: column \"foobar\" does not exist"), + }, + { + `invalid value`, + `{"level": "town1"}`, // Level is an integer column, but the value is a string. + nil, + errors.New("pq: invalid input syntax for type integer: \"town1\""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := filter.NewConverter(filter.WithArrayDriver(pq.Array)) + where, values, err := c.Convert([]byte(tt.input)) + if err != nil { + t.Fatal(err) + } + + rows, err := db.Query(` + SELECT id + FROM players + WHERE `+where+`; + `, values...) + if err != nil { + if tt.expectedError == nil { + t.Fatalf("unexpected error: %v", err) + } else if !strings.Contains(err.Error(), tt.expectedError.Error()) { + t.Fatalf("expected error %q, got %q", tt.expectedError, err) + } + return + } + defer rows.Close() + players := []int{} + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + t.Fatal(err) + } + players = append(players, id) + } + + if !reflect.DeepEqual(players, tt.expectedPlayers) { + t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where) + } + }) + } + + for op := range filter.BasicOperatorMap { + found := false + for _, tt := range tests { + if strings.Contains(tt.input, op) { + found = true + break + } + } + if !found { + t.Fatalf("operator %q is not tested", op) + } + } +} + +func TestIntegration_NestedJSONB(t *testing.T) { + db := setupPQ(t) + + createPlayersTable(t, db) + + tests := []struct { + name string + input string + expectedPlayers []int + }{ + { + "jsonb equals", + `{"guild_id": 20}`, + []int{1, 2}, + }, + { + "jsonb regex", + `{"pet": {"$regex": "^.{3}$"}}`, + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "excemption column", + `{"name": "Alice"}`, + []int{1}, + }, + { + "unknown column", + `{"foobar": "admin"}`, + []int{}, // Will always default to the jsonb column and return no results since it doesn't exist. + }, + { + "invalid value", + `{"guild_id": "dragon_slayers"}`, // Guild ID only contains integer values in the test data. + []int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := filter.NewConverter(filter.WithArrayDriver(pq.Array), filter.WithNestedJSONB("metadata", "name", "level", "class")) + where, values, err := c.Convert([]byte(tt.input)) + if err != nil { + t.Fatal(err) + } + + rows, err := db.Query(` + SELECT id + FROM players + WHERE `+where+`; + `, values...) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + players := []int{} + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + t.Fatal(err) + } + players = append(players, id) + } + + if !reflect.DeepEqual(players, tt.expectedPlayers) { + t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where) + } + }) + } +} + +func TestIntegration_Logic(t *testing.T) { + db := setupPQ(t) + + createPlayersTable(t, db) + + tests := []struct { + name string + input string + expectedPlayers []int + }{ + { + "basic or", + `{"$or": [{"level": {"$gt": 50}}, {"pet": "dog"}]}`, + []int{1, 3, 5, 6, 7, 8, 9, 10}, + }, + { + // (mages and (ends with E or ends with K)) or (dog owners and (guild in (50, 20))) + "complex triple nested", + `{"$or": [ + {"$and": [ + {"class": "mage"}, + {"$or": [ + {"name": {"$regex": "e$"}}, + {"name": {"$regex": "k$"}} + ]} + ]}, + {"$and": [ + {"pet": "dog"}, + {"guild_id": {"$in": [50, 20]}} + ]} + ]}`, + []int{1, 5, 7, 8}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := filter.NewConverter(filter.WithArrayDriver(pq.Array), filter.WithNestedJSONB("metadata", "name", "level", "class")) + where, values, err := c.Convert([]byte(tt.input)) + if err != nil { + t.Fatal(err) + } + + rows, err := db.Query(` + SELECT id + FROM players + WHERE `+where+`; + `, values...) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + players := []int{} + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + t.Fatal(err) + } + players = append(players, id) + } + + if !reflect.DeepEqual(players, tt.expectedPlayers) { + t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where) + } + }) + } +} diff --git a/integration/setup_test.go b/integration/setup_test.go index 13a4344..3303b6c 100644 --- a/integration/setup_test.go +++ b/integration/setup_test.go @@ -103,3 +103,36 @@ func setupDatabase(t *testing.T, connect func(string) error) { } }) } + +// createPlayersTable create a players table with 10 players. +func createPlayersTable(t *testing.T, db *sql.DB) { + t.Helper() + + if _, err := db.Exec(` + CREATE TABLE players ( + "id" serial PRIMARY KEY, + "name" text, + "metadata" jsonb, + "level" int, + "class" text + ); + `); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(` + INSERT INTO players ("id", "name", "metadata", "level", "class") + VALUES + (1, 'Alice', '{"guild_id": 20, "pet": "dog"}', 10, 'warrior'), + (2, 'Bob', '{"guild_id": 20, "pet": "cat"}', 20, 'mage'), + (3, 'Charlie', '{"guild_id": 30, "pet": "dog"}', 30, 'rogue'), + (4, 'David', '{"guild_id": 30, "pet": "cat"}', 40, 'warrior'), + (5, 'Eve', '{"guild_id": 40, "pet": "dog"}', 50, 'mage'), + (6, 'Frank', '{"guild_id": 40, "pet": "cat"}', 60, 'rogue'), + (7, 'Grace', '{"guild_id": 50, "pet": "dog"}', 70, 'warrior'), + (8, 'Hank', '{"guild_id": 50, "pet": "cat"}', 80, 'mage'), + (9, 'Ivy', '{"guild_id": 60, "pet": "dog"}', 90, 'rogue'), + (10, 'Jack', '{"guild_id": 60, "pet": "cat"}', 100, 'warrior') + `); err != nil { + t.Fatal(err) + } +}