From 5ea63ebac23b649074505f1319aec7ae58412944 Mon Sep 17 00:00:00 2001 From: martosaur Date: Fri, 5 Jul 2024 18:42:17 -0700 Subject: [PATCH 01/18] Create integrations_test/ and move that hit DB there --- integration_test/cases/flop_test.exs | 1768 ++++++++++++++++++++++++++ integration_test/pg/all_test.exs | 1 + integration_test/pg/test_helper.exs | 38 + mix.exs | 34 +- test/flop_test.exs | 1750 ------------------------- test/support/repo.ex | 5 - test/test_helper.exs | 2 - 7 files changed, 1839 insertions(+), 1759 deletions(-) create mode 100644 integration_test/cases/flop_test.exs create mode 100644 integration_test/pg/all_test.exs create mode 100644 integration_test/pg/test_helper.exs delete mode 100644 test/support/repo.ex diff --git a/integration_test/cases/flop_test.exs b/integration_test/cases/flop_test.exs new file mode 100644 index 00000000..b67b9814 --- /dev/null +++ b/integration_test/cases/flop_test.exs @@ -0,0 +1,1768 @@ +defmodule Flop.Integration.FlopTest do + use Flop.Integration.Case, async: Application.compile_env(:flop, :async_integration_tests, true) + use ExUnitProperties + + doctest Flop, import: true + + import Ecto.Query + import Flop.Factory + import Flop.Generators + import Flop.TestUtil + + alias __MODULE__.TestProvider + alias Ecto.Query.QueryExpr + alias Flop.Filter + alias Flop.Meta + alias Flop.Repo + alias MyApp.Owner + alias MyApp.Pet + + @pet_count_range 1..200 + + defmodule TestProvider do + use Flop, repo: Flop.Repo, default_limit: 35 + end + + defmodule TestProviderNested do + use Flop, + adapter_opts: [repo: Flop.Repo], + default_limit: 35 + end + + describe "ordering" do + test "adds order_by to query if set" do + pets = insert_list(20, :pet) + + expected = + Enum.sort( + pets, + &(&1.species < &2.species || + (&1.species == &2.species && &1.name >= &2.name)) + ) + + assert Flop.all(Pet, %Flop{ + order_by: [:species, :name], + order_directions: [:asc, :desc] + }) == expected + end + + test "uses :asc as default direction if no directions are passed" do + pets = insert_list(20, :pet) + expected = Enum.sort_by(pets, &{&1.species, &1.name, &1.age}) + + assert Flop.all(Pet, %Flop{ + order_by: [:species, :name, :age], + order_directions: nil + }) == expected + end + + test "uses :asc as default direction if not enough directions are passed" do + pets = insert_list(20, :pet) + + expected = + Enum.sort( + pets, + &(&1.species > &2.species || + (&1.species == &2.species && + (&1.name < &2.name || + (&1.name == &2.name && &1.age <= &2.age)))) + ) + + assert Flop.all(Pet, %Flop{ + order_by: [:species, :name, :age], + order_directions: [:desc] + }) == expected + end + + test "orders by join fields" do + pets = insert_list(20, :pet_with_owner) + + expected = + Enum.sort_by( + pets, + &{&1.owner.name, &1.owner.age, &1.name, &1.age} + ) + + result = + Pet + |> join(:left, [p], o in assoc(p, :owner), as: :owner) + |> preload(:owner) + |> Flop.all( + %Flop{order_by: [:owner_name, :owner_age, :name, :age]}, + for: Pet + ) + + assert result == expected + end + + test "orders by compound fields" do + pets = insert_list(20, :pet) + + expected = + Enum.sort_by( + pets, + &{&1.family_name, &1.given_name, &1.id} + ) + + result = Flop.all(Pet, %Flop{order_by: [:full_name, :id]}, for: Pet) + assert result == expected + end + + test "orders by compound fields with join fields" do + pets = insert_list(20, :pet, owner: fn -> build(:owner) end) + + expected = + pets |> Enum.map(&{&1.name, &1.owner.name, &1.id}) |> Enum.sort() + + q = + Pet + |> join(:left, [p], o in assoc(p, :owner), as: :owner) + |> select([p, owner: o], {p.name, o.name, p.id}) + + assert Flop.all( + q, + %Flop{order_by: [:pet_and_owner_name, :id]}, + for: Pet + ) == expected + + assert Flop.all( + q, + %Flop{ + order_by: [:pet_and_owner_name, :id], + order_directions: [:desc, :desc] + }, + for: Pet + ) == Enum.reverse(expected) + end + + test "orders by alias fields" do + owner_1 = insert(:owner, pets: build_list(2, :pet)) + owner_2 = insert(:owner, pets: build_list(1, :pet)) + owner_3 = insert(:owner, pets: build_list(4, :pet)) + owner_4 = insert(:owner, pets: build_list(3, :pet)) + + expected = [ + {owner_2.id, 1}, + {owner_1.id, 2}, + {owner_4.id, 3}, + {owner_3.id, 4} + ] + + q = + Owner + |> join(:left, [o], p in assoc(o, :pets), as: :pets) + |> group_by([o], o.id) + |> select( + [o, pets: p], + {o.id, p.id |> count() |> selected_as(:pet_count)} + ) + + assert Flop.all(q, %Flop{order_by: [:pet_count]}, for: Owner) == expected + + assert Flop.all( + q, + %Flop{order_by: [:pet_count], order_directions: [:desc]}, + for: Owner + ) == Enum.reverse(expected) + end + end + + describe "filtering" do + property "applies equality filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + # all except compound fields + field <- + member_of([:age, :name, :owner_age, :owner_name, :species]), + pet <- member_of(pets), + query_value <- pet |> Pet.get_field(field) |> constant(), + query_value != "" do + expected = filter_items(pets, field, :==, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :==, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + test "does not allow equality filter on compound fields" do + assert_raise Flop.InvalidParamsError, fn -> + query_pets_with_owners(%{ + filters: [%{field: :full_name, op: :==, value: "o"}] + }) + end + end + + property "applies inequality filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + # all except compound fields + field <- + member_of([:age, :name, :owner_age, :owner_name, :species]), + pet <- member_of(pets), + query_value = Pet.get_field(pet, field), + query_value != "" do + expected = filter_items(pets, field, :!=, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :!=, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + test "does not allow inequality filter on compound fields" do + assert_raise Flop.InvalidParamsError, fn -> + query_pets_with_owners(%{ + filters: [%{field: :full_name, op: :!=, value: "o"}] + }) + end + end + + test "applies empty and not_empty filter" do + check all pet_count <- integer(@pet_count_range), + pets = + insert_list_and_sort(pet_count, :pet, + species: fn -> Enum.random([nil, "fox"]) end, + owner: fn -> + build(:owner, name: fn -> Enum.random([nil, "Carl"]) end) + end + ), + field <- member_of([:species, :owner_name]), + op <- member_of([:empty, :not_empty]) do + [opposite_op] = [:empty, :not_empty] -- [op] + expected = filter_items(pets, field, op) + opposite_expected = filter_items(pets, field, opposite_op) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: true}] + }) == expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: false}] + }) == opposite_expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: true}] + }) == opposite_expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: false}] + }) == expected + + checkin_checkout() + end + end + + # test "applies empty filter" do + # require Flop.Adapter.Ecto.Operators + + # field = :species + + # d1 = dynamic([r], is_nil(field(r, ^field)) == ^true); d2 = dynamic([r], Flop.Adapter.Ecto.Operators.empty(:other) == ^true) + + # assert where(Pet, ^d1) == where(Pet, ^d2) + # end + + test "applies empty and not_empty filter with string values" do + check all pet_count <- integer(@pet_count_range), + pets = + insert_list_and_sort(pet_count, :pet, + species: fn -> Enum.random([nil, "fox"]) end, + owner: fn -> + build(:owner, name: fn -> Enum.random([nil, "Carl"]) end) + end + ), + field <- member_of([:species, :owner_name]), + op <- member_of([:empty, :not_empty]) do + [opposite_op] = [:empty, :not_empty] -- [op] + expected = filter_items(pets, field, op) + opposite_expected = filter_items(pets, field, opposite_op) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: "true"}] + }) == expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: "false"}] + }) == opposite_expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: "true"}] + }) == opposite_expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: "false"}] + }) == expected + + checkin_checkout() + end + end + + test "applies empty and not_empty filter to array fields" do + check all pet_count <- integer(@pet_count_range), + pets = + insert_list_and_sort(pet_count, :pet_with_owner, + tags: fn -> Enum.random([nil, [], ["catdog"]]) end, + owner: fn -> + build(:owner, + tags: fn -> Enum.random([nil, [], ["catlover"]]) end + ) + end + ), + field <- member_of([:tags, :owner_tags]), + op <- member_of([:empty, :not_empty]) do + [opposite_op] = [:empty, :not_empty] -- [op] + expected = filter_items(pets, field, op, true) + opposite_expected = filter_items(pets, field, opposite_op, true) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: true}] + }) == expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: false}] + }) == opposite_expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: true}] + }) == opposite_expected + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: false}] + }) == expected + + checkin_checkout() + end + end + + test "applies empty and not_empty filter to map fields" do + check all fruit_count <- integer(@pet_count_range), + fruits = + insert_list_and_sort(fruit_count, :fruit, + attributes: fn -> Enum.random([nil, %{}, %{"a" => "b"}]) end, + extra: fn -> Enum.random([nil, %{}, %{"a" => "b"}]) end, + owner: + build(:owner, + attributes: fn -> + Enum.random([nil, %{}, %{"a" => "b"}]) + end, + extra: fn -> Enum.random([nil, %{}, %{"a" => "b"}]) end + ) + ), + field <- + member_of([ + :attributes, + :extra, + :owner_attributes, + :owner_extra + ]), + op <- member_of([:empty, :not_empty]) do + [opposite_op] = [:empty, :not_empty] -- [op] + expected = filter_items(fruits, field, op, true) + opposite_expected = filter_items(fruits, field, opposite_op, true) + + assert query_fruits_with_owners(%{ + filters: [%{field: field, op: op, value: true}] + }) == expected + + assert query_fruits_with_owners(%{ + filters: [%{field: field, op: op, value: false}] + }) == opposite_expected + + assert query_fruits_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: true}] + }) == opposite_expected + + assert query_fruits_with_owners(%{ + filters: [%{field: field, op: opposite_op, value: false}] + }) == expected + + checkin_checkout() + end + end + + property "applies like filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + query_value <- substring(value) do + expected = filter_items(pets, field, :like, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :like, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + test "escapes % in (i)like queries" do + %{id: _id1} = insert(:pet, name: "abc") + %{id: id2} = insert(:pet, name: "a%c") + + for op <- [:like, :ilike, :like_and, :like_or, :ilike_and, :ilike_or] do + flop = %Flop{filters: [%Filter{field: :name, op: op, value: "a%c"}]} + assert [%Pet{id: ^id2}] = Flop.all(Pet, flop) + end + end + + test "escapes _ in (i)like queries" do + %{id: _id1} = insert(:pet, name: "abc") + %{id: id2} = insert(:pet, name: "a_c") + + for op <- [:like, :ilike, :like_and, :like_or, :ilike_and, :ilike_or] do + flop = %Flop{filters: [%Filter{field: :name, op: op, value: "a_c"}]} + assert [%Pet{id: ^id2}] = Flop.all(Pet, flop) + end + end + + test "escapes \\ in (i)like queries" do + %{id: _id1} = insert(:pet, name: "abc") + %{id: id2} = insert(:pet, name: "a\\c") + + for op <- [:like, :ilike, :like_and, :like_or, :ilike_and, :ilike_or] do + flop = %Flop{filters: [%Filter{field: :name, op: op, value: "a\\c"}]} + assert [%Pet{id: ^id2}] = Flop.all(Pet, flop) + end + end + + property "applies not like filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + query_value <- substring(value) do + expected = filter_items(pets, field, :not_like, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :not_like, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies ilike filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + op <- member_of([:=~, :ilike]), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + query_value <- substring(value) do + expected = filter_items(pets, field, :ilike, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies not ilike filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + query_value <- substring(value) do + expected = filter_items(pets, field, :not_ilike, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :not_ilike, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies like_and filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + search_text_or_list <- search_text_or_list(value) do + expected = filter_items(pets, field, :like_and, search_text_or_list) + + assert query_pets_with_owners(%{ + filters: [ + %{field: field, op: :like_and, value: search_text_or_list} + ] + }) == expected + + checkin_checkout() + end + end + + property "applies like_or filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + search_text_or_list <- search_text_or_list(value) do + expected = filter_items(pets, field, :like_or, search_text_or_list) + + assert query_pets_with_owners(%{ + filters: [ + %{field: field, op: :like_or, value: search_text_or_list} + ] + }) == expected + + checkin_checkout() + end + end + + property "applies ilike_and filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + search_text_or_list <- search_text_or_list(value) do + expected = filter_items(pets, field, :ilike_and, search_text_or_list) + + assert query_pets_with_owners(%{ + filters: [ + %{field: field, op: :ilike_and, value: search_text_or_list} + ] + }) == expected + + checkin_checkout() + end + end + + property "applies ilike_or filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- filterable_pet_field(:string), + pet <- member_of(pets), + value = Pet.get_field(pet, field), + search_text_or_list <- search_text_or_list(value) do + expected = filter_items(pets, field, :ilike_or, search_text_or_list) + + assert query_pets_with_owners(%{ + filters: [ + %{field: field, op: :ilike_or, value: search_text_or_list} + ] + }) == expected + + checkin_checkout() + end + end + + property "applies lte, lt, gt and gte filters" do + check all pet_count <- integer(@pet_count_range), + pets = + pet_count + |> insert_list(:pet_downcase, owner: fn -> build(:owner) end) + |> Enum.sort_by(& &1.id), + field <- member_of([:age, :name, :owner_age]), + op <- one_of([:<=, :<, :>, :>=]), + query_value <- compare_value_by_field(field) do + expected = filter_items(pets, field, op, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: op, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies :in operator" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- member_of([:age, :name, :owner_age]), + values = Enum.map(pets, &Map.get(&1, field)), + query_value <- + list_of(one_of([member_of(values), value_by_field(field)]), + max_length: 5 + ) do + expected = filter_items(pets, field, :in, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :in, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies :not_in operator" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- member_of([:age, :name, :owner_age]), + values = Enum.map(pets, &Map.get(&1, field)), + query_value <- + list_of(one_of([member_of(values), value_by_field(field)]), + max_length: 5 + ) do + expected = filter_items(pets, field, :not_in, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :not_in, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies :contains operator" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- member_of([:tags, :owner_tags]), + values = Enum.flat_map(pets, &Pet.get_field(&1, field)), + query_value <- member_of(values) do + expected = filter_items(pets, field, :contains, query_value) + + assert query_pets_with_owners(%{ + filters: [%{field: field, op: :contains, value: query_value}] + }) == expected + + checkin_checkout() + end + end + + property "applies :not_contains operator" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + field <- member_of([:tags, :owner_tags]), + values = Enum.flat_map(pets, &Pet.get_field(&1, field)), + query_value <- member_of(values) do + expected = filter_items(pets, field, :not_contains, query_value) + + assert query_pets_with_owners(%{ + filters: [ + %{field: field, op: :not_contains, value: query_value} + ] + }) == expected + + checkin_checkout() + end + end + + property "custom field filter" do + check all pet_count <- integer(@pet_count_range), + pets = insert_list_and_sort(pet_count, :pet_with_owner), + values = Enum.map(pets, &String.reverse(&1.name)), + query_value <- member_of(values) do + expected = filter_items(pets, :name, :==, query_value) + + assert query_pets_with_owners(%{ + filters: [ + %{field: :reverse_name, op: :==, value: query_value} + ] + }) == expected + + checkin_checkout() + end + end + + test "filtering with custom fields" do + pets = insert_list_and_sort(1, :pet_with_owner) + + assert [] = + query_pets_with_owners(%{ + filters: [ + %{field: :custom, op: :==, value: "some_value"} + ] + }) + + receive do + {:filter, filter} -> + assert filter == + {%Flop.Filter{field: :custom, op: :==, value: "some_value"}, + [some: :options]} + end + + assert ^pets = + query_pets_with_owners( + %{ + filters: [ + %{field: :custom, op: :==, value: "some_other_value"} + ] + }, + extra_opts: [other: :options] + ) + + receive do + {:filter, filter} -> + assert filter == + {%Flop.Filter{ + field: :custom, + op: :==, + value: "some_other_value" + }, [other: :options, some: :options]} + end + end + + test "silently ignores nil values for field and value" do + flop = %Flop{filters: [%Filter{op: :>=, value: 4}]} + assert Flop.query(Pet, flop) == Pet + + flop = %Flop{filters: [%Filter{field: :name, op: :>=}]} + assert Flop.query(Pet, flop) == Pet + end + + test "leaves query unchanged if everything is nil" do + flop = %Flop{ + filters: nil, + limit: nil, + offset: nil, + order_by: nil, + order_directions: nil, + page: nil, + page_size: nil + } + + assert Flop.query(Pet, flop) == Pet + end + end + + describe "all/3" do + test "returns all matching entries" do + matching_pets = insert_list(6, :pet, age: 5) + _non_matching_pets = insert_list(4, :pet, age: 6) + + [_, _, %{name: name_1}, %{name: name_2}, _, _] = + Enum.sort_by(matching_pets, & &1.name) + + flop = %Flop{ + limit: 2, + offset: 2, + order_by: [:name], + filters: [%Filter{field: :age, op: :<=, value: 5}] + } + + assert Enum.map(Flop.all(Pet, flop), & &1.name) == [name_1, name_2] + end + + test "can apply a query prefix" do + insert(:pet, %{}, prefix: "other_schema") + + assert Flop.all(Pet, %Flop{}) == [] + refute Flop.all(Pet, %Flop{}, query_opts: [prefix: "other_schema"]) == [] + end + end + + describe "count/3" do + test "returns count of matching entries" do + _matching_pets = insert_list(6, :pet, age: 5) + _non_matching_pets = insert_list(4, :pet, age: 6) + + flop = %Flop{ + limit: 2, + offset: 2, + order_by: [:age], + filters: [%Filter{field: :age, op: :<=, value: 5}] + } + + assert Flop.count(Pet, flop) == 6 + end + + test "can apply a query prefix" do + insert(:pet, %{}, prefix: "other_schema") + + assert Flop.count(Pet, %Flop{}) == 0 + assert Flop.count(Pet, %Flop{}, query_opts: [prefix: "other_schema"]) == 1 + end + + test "allows overriding query" do + _matching_pets = insert_list(6, :pet, age: 5, name: "A") + _more_matching_pets = insert_list(5, :pet, age: 5, name: "B") + _non_matching_pets = insert_list(4, :pet, age: 6) + + flop = %Flop{ + limit: 2, + offset: 2, + order_by: [:age], + filters: [%Filter{field: :age, op: :<=, value: 5}] + } + + # default query + assert Flop.count(Pet, flop) == 11 + + # custom count query + assert Flop.count(Pet, flop, count_query: where(Pet, name: "A")) == 6 + assert Flop.count(Pet, flop, count_query: where(Pet, name: "B")) == 5 + end + + test "allows overriding the count itself" do + _matching_pets = insert_list(6, :pet, age: 5, name: "A") + _more_matching_pets = insert_list(5, :pet, age: 5, name: "B") + _non_matching_pets = insert_list(4, :pet, age: 6) + + flop = %Flop{ + limit: 2, + offset: 2, + order_by: [:age], + filters: [%Filter{field: :age, op: :<=, value: 5}] + } + + # default query + assert Flop.count(Pet, flop) == 11 + + # custom count + assert Flop.count(Pet, flop, count: 6) == 6 + end + + test "counts entries for queries with group_by clauses" do + _owner_1 = insert(:owner, age: 13, pets: build_list(3, :pet)) + _owner_2 = insert(:owner, age: 20, pets: build_list(2, :pet)) + _owner_3 = insert(:owner, age: 22, pets: build_list(1, :pet)) + _non_matching_owner = insert(:owner, age: 52, pets: build_list(4, :pet)) + + q = + Owner + |> join(:left, [o], p in assoc(o, :pets), as: :pets) + |> group_by([o], o.id) + |> select( + [o, pets: p], + %{o | pet_count: count(p.id)} + ) + + flop = %Flop{ + filters: [%Filter{field: :age, op: :<=, value: 30}] + } + + assert Flop.count(q, flop) == 3 + end + end + + describe "meta/3" do + test "returns the meta information for a query with limit/offset" do + _matching_pets = insert_list(7, :pet, age: 5) + _non_matching_pets = insert_list(4, :pet, age: 6) + + flop = %Flop{ + limit: 2, + offset: 4, + filters: [%Filter{field: :age, op: :<=, value: 5}] + } + + assert Flop.meta(Pet, flop) == %Meta{ + current_offset: 4, + current_page: 3, + end_cursor: nil, + flop: flop, + has_next_page?: true, + has_previous_page?: true, + next_offset: 6, + next_page: 4, + page_size: 2, + previous_offset: 2, + previous_page: 2, + start_cursor: nil, + total_count: 7, + total_pages: 4 + } + end + + test "returns the meta information for a query with page/page_size" do + _matching_pets = insert_list(7, :pet, age: 5) + _non_matching_pets = insert_list(4, :pet, age: 6) + + flop = %Flop{ + page_size: 2, + page: 3, + filters: [%Filter{field: :age, op: :<=, value: 5}] + } + + assert Flop.meta(Pet, flop) == %Meta{ + current_offset: 4, + current_page: 3, + end_cursor: nil, + flop: flop, + has_next_page?: true, + has_previous_page?: true, + next_offset: 6, + next_page: 4, + page_size: 2, + previous_offset: 2, + previous_page: 2, + start_cursor: nil, + total_count: 7, + total_pages: 4 + } + end + + test "returns the meta information for a query without limit" do + _matching_pets = insert_list(7, :pet, age: 5) + _non_matching_pets = insert_list(2, :pet, age: 6) + + flop = %Flop{filters: [%Filter{field: :age, op: :<=, value: 5}]} + + assert Flop.meta(Pet, flop) == %Meta{ + current_offset: 0, + current_page: 1, + end_cursor: nil, + flop: flop, + has_next_page?: false, + has_previous_page?: false, + next_offset: nil, + next_page: nil, + page_size: nil, + previous_offset: nil, + previous_page: nil, + start_cursor: nil, + total_count: 7, + total_pages: 1 + } + end + + test "rounds current page if offset is between pages" do + insert_list(6, :pet) + + assert %Meta{ + current_offset: 1, + current_page: 2, + has_next_page?: true, + has_previous_page?: true, + next_offset: 3, + next_page: 3, + previous_offset: 0, + previous_page: 1 + } = Flop.meta(Pet, %Flop{limit: 2, offset: 1}) + + assert %Meta{ + current_offset: 3, + current_page: 3, + has_next_page?: true, + has_previous_page?: true, + next_offset: 5, + next_page: 3, + previous_offset: 1, + previous_page: 2 + } = Flop.meta(Pet, %Flop{limit: 2, offset: 3}) + + # current page shouldn't be greater than total page numbers + assert %Meta{ + current_offset: 5, + current_page: 3, + has_next_page?: false, + has_previous_page?: true, + next_offset: nil, + next_page: nil, + previous_offset: 3, + previous_page: 2 + } = Flop.meta(Pet, %Flop{limit: 2, offset: 5}) + end + + test "sets has_previous_page? and has_next_page?" do + _matching_pets = insert_list(5, :pet) + + assert %Meta{has_next_page?: true, has_previous_page?: false} = + Flop.meta(Pet, %Flop{limit: 2, offset: 0}) + + assert %Meta{has_next_page?: true, has_previous_page?: true} = + Flop.meta(Pet, %Flop{limit: 2, offset: 1}) + + assert %Meta{has_next_page?: true, has_previous_page?: true} = + Flop.meta(Pet, %Flop{limit: 2, offset: 2}) + + assert %Meta{has_next_page?: false, has_previous_page?: true} = + Flop.meta(Pet, %Flop{limit: 2, offset: 3}) + + assert %Meta{has_next_page?: false, has_previous_page?: true} = + Flop.meta(Pet, %Flop{limit: 2, offset: 4}) + + assert %Meta{has_next_page?: true, has_previous_page?: false} = + Flop.meta(Pet, %Flop{page_size: 3, page: 1}) + + assert %Meta{has_next_page?: false, has_previous_page?: true} = + Flop.meta(Pet, %Flop{page_size: 3, page: 2}) + end + + test "can apply a query prefix" do + insert(:pet, %{}, prefix: "other_schema") + + assert Flop.meta(Pet, %Flop{}).total_count == 0 + + assert Flop.meta( + Pet, + %Flop{}, + query_opts: [prefix: "other_schema"] + ).total_count == 1 + end + + test "sets the schema if :for option is passed" do + assert Flop.meta(Pet, %Flop{}).schema == nil + assert Flop.meta(Pet, %Flop{}, for: Pet).schema == Pet + end + + test "sets options" do + opts = Flop.meta(Pet, %Flop{}, for: Pet).opts + assert opts[:for] == Pet + end + end + + describe "run/3" do + test "returns data and meta data" do + insert_list(3, :pet) + flop = %Flop{page_size: 2, page: 2} + assert {[%Pet{}], %Meta{}} = Flop.run(Pet, flop) + end + end + + describe "validate_and_run/3" do + test "returns error if flop is invalid" do + flop = %Flop{ + page_size: -1, + filters: [%Filter{field: :name, op: :something_like}] + } + + assert {:error, %Meta{} = meta} = Flop.validate_and_run(Pet, flop) + + assert meta.params == %{ + "page_size" => -1, + "filters" => [%{"field" => :name, "op" => :something_like}] + } + + assert [filters: [_], page_size: [_]] = meta.errors + end + + test "returns data and meta data" do + insert_list(3, :pet) + flop = %{page_size: 2, page: 2} + assert {:ok, {[%Pet{}], %Meta{}}} = Flop.validate_and_run(Pet, flop) + end + end + + describe "validate_and_run!/3" do + test "raises if flop is invalid" do + assert_raise Flop.InvalidParamsError, fn -> + Flop.validate_and_run!(Pet, %{limit: -1}) + end + end + + test "returns data and meta data" do + insert_list(3, :pet) + flop = %{page_size: 2, page: 2} + assert {[%Pet{}], %Meta{}} = Flop.validate_and_run!(Pet, flop) + end + end + + describe "offset-based pagination" do + test "applies limit to query" do + insert_list(6, :pet) + assert Pet |> Flop.query(%Flop{limit: 4}) |> Repo.all() |> length() == 4 + end + + test "applies offset to query if set" do + pets = insert_list(10, :pet) + + expected_pets = + pets + |> Enum.sort_by(&{&1.name, &1.species, &1.age}) + |> Enum.slice(4..10) + + flop = %Flop{offset: 4, order_by: [:name, :species, :age]} + query = Flop.query(Pet, flop) + assert %QueryExpr{params: [{4, :integer}]} = query.offset + assert Repo.all(query) == expected_pets + end + + test "applies limit and offset to query if page and page size are set" do + pets = insert_list(40, :pet) + sorted_pets = Enum.sort_by(pets, &{&1.name, &1.species, &1.age}) + order_by = [:name, :species, :age] + + flop = %Flop{page: 1, page_size: 10, order_by: order_by} + query = Flop.query(Pet, flop) + assert %QueryExpr{params: [{0, :integer}]} = query.offset + assert %{params: [{10, :integer}]} = query.limit + assert Repo.all(query) == Enum.slice(sorted_pets, 0..9) + + flop = %Flop{page: 2, page_size: 10, order_by: order_by} + query = Flop.query(Pet, flop) + assert %QueryExpr{params: [{10, :integer}]} = query.offset + assert %{params: [{10, :integer}]} = query.limit + assert Repo.all(query) == Enum.slice(sorted_pets, 10..19) + + flop = %Flop{page: 3, page_size: 4, order_by: order_by} + query = Flop.query(Pet, flop) + assert %QueryExpr{params: [{8, :integer}]} = query.offset + assert %{params: [{4, :integer}]} = query.limit + assert Repo.all(query) == Enum.slice(sorted_pets, 8..11) + end + end + + describe "cursor pagination" do + property "querying cursor by cursor forward includes all items in order" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}) do + checkin_checkout() + + # insert pets into DB, retrieve them so we have the IDs + Enum.each(pets, &Repo.insert!(&1)) + + pets = + Flop.all( + pets_with_owners_query(), + %Flop{order_by: cursor_fields, order_directions: directions}, + for: Pet + ) + + # retrieve first cursor, ensure returned pet matches first one in list + [first_pet | remaining_pets] = pets + + {:ok, {[returned_pet], %Meta{end_cursor: cursor}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + assert returned_pet == first_pet + + # iterate over remaining pets, query DB cursor by cursor + {reversed_returned_pets, last_cursor} = + Enum.reduce( + remaining_pets, + {[first_pet], cursor}, + fn _current_pet, {pet_list, cursor} -> + assert {:ok, + {[returned_pet], + %Meta{ + end_cursor: new_cursor, + flop: %Flop{decoded_cursor: nil} + }}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + after: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + {[returned_pet | pet_list], new_cursor} + end + ) + + # ensure the accumulated list matches the manually sorted list + returned_pets = Enum.reverse(reversed_returned_pets) + + assert returned_pets == pets + + # ensure nothing comes after the last cursor + assert {:ok, {[], %Meta{end_cursor: nil}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + after: last_cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "querying all items returns same list forward and backward" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + pet_count = length(pets) + + {:ok, {with_first, _meta}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: pet_count, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + {:ok, {with_last, _meta}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + last: pet_count, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + assert with_first == with_last + end + end + + property "querying cursor by cursor backward includes all items in order" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}) do + checkin_checkout() + + # insert pets into DB, retrieve them so we have the IDs + Enum.each(pets, &Repo.insert!(&1)) + + pets = + Flop.all( + pets_with_owners_query(), + %Flop{order_by: cursor_fields, order_directions: directions}, + for: Pet + ) + + pets = Enum.reverse(pets) + + # retrieve last cursor, ensure returned pet matches last one in list + [last_pet | remaining_pets] = pets + + {:ok, {[returned_pet], %Meta{end_cursor: cursor}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + last: 1, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + assert returned_pet == last_pet + + # iterate over remaining pets, query DB cursor by cursor + {reversed_returned_pets, last_cursor} = + Enum.reduce( + remaining_pets, + {[last_pet], cursor}, + fn _current_pet, {pet_list, cursor} -> + assert {:ok, {[returned_pet], %Meta{end_cursor: new_cursor}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + last: 1, + before: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + {[returned_pet | pet_list], new_cursor} + end + ) + + # ensure the accumulated list matches the manually sorted list + returned_pets = Enum.reverse(reversed_returned_pets) + assert returned_pets == pets + + # ensure nothing comes after the last cursor + assert {:ok, {[], %Meta{end_cursor: nil}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + last: 1, + before: last_cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_previous_page? is false without after and last" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + first <- integer(1..(length(pets) + 1)) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + + assert {_, %Meta{has_previous_page?: false}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + first: first, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_previous_page? is true with after" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + first <- integer(1..(length(pets) + 1)), + cursor_pet <- member_of(pets) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + + cursor = + cursor_fields + |> Enum.into(%{}, fn field -> + {field, Pet.get_field(cursor_pet, field)} + end) + |> Flop.Cursor.encode() + + assert {_, %Meta{has_previous_page?: true}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + first: first, + after: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_previous_page? is true with last set and items left" do + check all pets <- uniq_list_of_pets(length: 3..50), + pet_count = length(pets), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + last <- integer(1..(pet_count - 2)), + cursor_index <- integer((last + 1)..(pet_count - 1)) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + + # retrieve ordered pets + pets = + Flop.all( + pets_with_owners_query(), + %Flop{order_by: cursor_fields, order_directions: directions}, + for: Pet + ) + + # retrieve cursor + pet = Enum.at(pets, cursor_index) + + cursor = + cursor_fields + |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) + |> Flop.Cursor.encode() + + assert {_, %Meta{has_previous_page?: true}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + last: last, + before: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_previous_page? is false with last set and no items left" do + check all pets <- uniq_list_of_pets(length: 3..50), + pet_count = length(pets), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + # include test with limits greater than item count + last <- integer(1..(pet_count + 20)), + cursor_index <- integer(0..min(pet_count - 1, last)) do + checkin_checkout() + + # insert pets + Enum.each(pets, &Repo.insert!(&1)) + + # retrieve ordered pets + pets = + Flop.all( + pets_with_owners_query(), + %Flop{order_by: cursor_fields, order_directions: directions}, + for: Pet + ) + + # retrieve cursor + pet = Enum.at(pets, cursor_index) + + cursor = + cursor_fields + |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) + |> Flop.Cursor.encode() + + assert {_, %Meta{has_previous_page?: false}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + last: last, + before: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_next_page? is false without first and before" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + last <- integer(1..(length(pets) + 1)) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + + assert {_, %Meta{has_next_page?: false}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + last: last, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_next_page? is true with before" do + check all pets <- uniq_list_of_pets(length: 1..25), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + last <- integer(1..(length(pets) + 1)), + cursor_pet <- member_of(pets) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + + cursor = + cursor_fields + |> Enum.into(%{}, fn field -> + {field, Pet.get_field(cursor_pet, field)} + end) + |> Flop.Cursor.encode() + + assert {_, %Meta{has_next_page?: true}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + last: last, + before: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_next_page? is true with first set and items left" do + check all pets <- uniq_list_of_pets(length: 3..50), + pet_count = length(pets), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + first <- integer(1..(pet_count - 2)), + cursor_index <- integer((first + 1)..(pet_count - 1)) do + checkin_checkout() + Enum.each(pets, &Repo.insert!(&1)) + + # retrieve ordered pets + pets = + pets_with_owners_query() + |> Flop.all( + %Flop{order_by: cursor_fields, order_directions: directions}, + for: Pet + ) + |> Enum.reverse() + + # retrieve cursor + pet = Enum.at(pets, cursor_index) + + cursor = + cursor_fields + |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) + |> Flop.Cursor.encode() + + assert {_, %Meta{has_next_page?: true}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + first: first, + after: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + property "has_next_page? is false with first set and no items left" do + check all pet_count <- integer(3..50), + pets <- uniq_list_of_pets(length: pet_count..pet_count), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}), + # include test with limits greater than item count + first <- integer(1..(pet_count + 20)), + cursor_index <- + integer(max(0, pet_count - first)..(pet_count - 1)) do + checkin_checkout() + + Enum.each(pets, &Repo.insert!(&1)) + + # retrieve ordered pets + pets = + Flop.all( + pets_with_owners_query(), + %Flop{order_by: cursor_fields, order_directions: directions}, + for: Pet + ) + + # retrieve cursor + pet = Enum.at(pets, cursor_index) + + cursor = + cursor_fields + |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) + |> Flop.Cursor.encode() + + assert {_, %Meta{has_next_page?: false}} = + Flop.validate_and_run!( + pets_with_owners_query(), + %Flop{ + first: first, + after: cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + test "cursor value function can be overridden" do + insert_list(4, :pet) + query = select(Pet, [p], {p, %{other: :data}}) + + cursor_value_func = fn {pet, _}, order_by -> + Map.take(pet, order_by) + end + + {:ok, + {_r1, + %Meta{ + end_cursor: end_cursor, + has_next_page?: true + }}} = + Flop.validate_and_run( + query, + %Flop{first: 2, order_by: [:id]}, + cursor_value_func: cursor_value_func + ) + + {:ok, + {_r2, + %Meta{ + end_cursor: _end_cursor, + has_next_page?: false + }}} = + Flop.validate_and_run( + query, + %Flop{first: 2, after: end_cursor, order_by: [:id]}, + cursor_value_func: cursor_value_func + ) + end + + test "nil values for cursors are ignored when using for option" do + check all pets <- uniq_list_of_pets(length: 2..2), + cursor_fields <- cursor_fields(%Pet{}), + directions <- order_directions(%Pet{}) do + checkin_checkout() + + # set name fields to nil and insert + pets + |> Enum.map(&Map.update!(&1, :name, fn _ -> nil end)) + |> Enum.each(&Repo.insert!(&1)) + + assert {:ok, {[_], %Meta{end_cursor: end_cursor}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + + assert {:ok, _} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + after: end_cursor, + order_by: cursor_fields, + order_directions: directions + }, + for: Pet + ) + end + end + + test "raises if alias field is used" do + q = + Owner + |> join(:left, [o], p in assoc(o, :pets), as: :pets) + |> group_by([o], o.id) + |> select( + [o, pets: p], + %{o | pet_count: p.id |> count() |> selected_as(:pet_count)} + ) + + insert(:owner) + + assert {_, %Meta{end_cursor: end_cursor}} = + Flop.run( + q, + %Flop{first: 1, order_by: [:pet_count, :id]}, + for: Owner + ) + + error = + assert_raise RuntimeError, + fn -> + Flop.run( + q, + %Flop{ + first: 1, + after: end_cursor, + order_by: [:pet_count, :id] + }, + for: Owner + ) + end + + assert error.message =~ + "alias fields are not supported in cursor pagination" + end + + test "nil values for cursors are ignored when not using for option" do + check all pets <- uniq_list_of_pets(length: 2..2), + directions <- order_directions(%Pet{}) do + checkin_checkout() + cursor_fields = [:name, :age] + + # set name fields to nil and insert + pets + |> Enum.map(&Map.update!(&1, :name, fn _ -> nil end)) + |> Enum.each(&Repo.insert!(&1)) + + assert {:ok, {[_], %Meta{end_cursor: end_cursor}}} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + order_by: cursor_fields, + order_directions: directions + } + ) + + assert {:ok, _} = + Flop.validate_and_run( + pets_with_owners_query(), + %Flop{ + first: 1, + after: end_cursor, + order_by: cursor_fields, + order_directions: directions + } + ) + end + end + + test "paging on composite type" do + 1..10 + |> Enum.map( + &MyApp.WalkingDistances.changeset(%MyApp.WalkingDistances{}, %{ + trip: %{value: &1, unit: "m"} + }) + ) + |> Enum.each(&Repo.insert!(&1)) + + assert {:ok, {[_element], %Meta{end_cursor: end_cursor}}} = + Flop.validate_and_run( + MyApp.WalkingDistances, + %Flop{first: 1, order_by: [:trip]}, + for: MyApp.WalkingDistances + ) + + assert {:ok, {[_element], %Meta{}}} = + Flop.validate_and_run( + MyApp.WalkingDistances, + %Flop{first: 1, after: end_cursor, order_by: [:trip]}, + for: MyApp.WalkingDistances + ) + end + end + + describe "__using__/1" do + test "defines wrapper functions that pass default options" do + insert_list(3, :pet) + + assert {:ok, {_, %Meta{page_size: 35}}} = + TestProvider.validate_and_run(Pet, %{}) + end + + test "allows to override defaults" do + insert_list(3, :pet) + + assert {:ok, {_, %Meta{page_size: 30}}} = + TestProvider.validate_and_run(Pet, %{page_size: 30}) + end + + test "passes backend module" do + assert {:ok, {_, %Meta{backend: TestProvider, opts: opts}}} = + TestProvider.validate_and_run(Pet, %{}) + + assert Keyword.get(opts, :backend) == TestProvider + end + end + + describe "__using__/1 with nested adapter options" do + test "defines wrapper functions that pass default options" do + insert_list(3, :pet) + + assert {:ok, {_, %Meta{page_size: 35}}} = + TestProviderNested.validate_and_run(Pet, %{}) + end + + test "allows to override defaults" do + insert_list(3, :pet) + + assert {:ok, {_, %Meta{page_size: 30}}} = + TestProviderNested.validate_and_run(Pet, %{page_size: 30}) + end + + test "passes backend module" do + assert {:ok, {_, %Meta{backend: TestProviderNested, opts: opts}}} = + TestProviderNested.validate_and_run(Pet, %{}) + + assert Keyword.get(opts, :backend) == TestProviderNested + end + end +end \ No newline at end of file diff --git a/integration_test/pg/all_test.exs b/integration_test/pg/all_test.exs new file mode 100644 index 00000000..beb5cfcd --- /dev/null +++ b/integration_test/pg/all_test.exs @@ -0,0 +1 @@ +Code.require_file "../cases/flop_test.exs", __DIR__ \ No newline at end of file diff --git a/integration_test/pg/test_helper.exs b/integration_test/pg/test_helper.exs new file mode 100644 index 00000000..3f332519 --- /dev/null +++ b/integration_test/pg/test_helper.exs @@ -0,0 +1,38 @@ +Application.put_env(:flop, :async_integration_tests, true) + +# Configure PG connection +Application.put_env(:flop, Flop.Repo, + username: "postgres", + password: "postgres", + database: "flop_test#{System.get_env("MIX_TEST_PARTITION")}", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox +) + +defmodule Flop.Repo do + use Ecto.Repo, + otp_app: :flop, + adapter: Ecto.Adapters.Postgres +end + +defmodule Flop.Integration.Case do + use ExUnit.CaseTemplate + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Flop.Repo) + end +end + +{:ok, _} = Ecto.Adapters.Postgres.ensure_all_started(Flop.Repo.config(), :temporary) + +# Load up the repository, start it, and run migrations +_ = Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) +:ok = Ecto.Adapters.Postgres.storage_up(Flop.Repo.config()) + +{:ok, _pid} = Flop.Repo.start_link() + +[_ | _] = Ecto.Migrator.run(Flop.Repo, :up, log: true, all: true) +Ecto.Adapters.SQL.Sandbox.mode(Flop.Repo, :manual) + +{:ok, _} = Application.ensure_all_started(:ex_machina) +ExUnit.start() \ No newline at end of file diff --git a/mix.exs b/mix.exs index 4761931b..5aeca88d 100644 --- a/mix.exs +++ b/mix.exs @@ -3,6 +3,7 @@ defmodule Flop.MixProject do @source_url "https://github.com/woylie/flop" @version "0.25.0" + @adapters ~w(pg) def project do [ @@ -13,6 +14,7 @@ defmodule Flop.MixProject do elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), test_coverage: [tool: ExCoveralls], + test_paths: test_paths(System.get_env("ECTO_ADAPTER")), preferred_cli_env: [ "coveralls.detail": :test, "coveralls.github": :test, @@ -23,6 +25,8 @@ defmodule Flop.MixProject do "ecto.drop": :test, "ecto.migrate": :test, "ecto.reset": :test, + "test.all": :test, + "test.adapters": :test, coveralls: :test, dialyzer: :test ], @@ -104,8 +108,34 @@ defmodule Flop.MixProject do defp aliases do [ - "ecto.reset": ["ecto.drop", "ecto.create --quiet", "ecto.migrate"], - test: ["ecto.create --quiet", "ecto.migrate", "test"] + "test.all": ["test", "test.adapters"], + "test.adapters": &test_adapters/1 ] end + + defp test_paths(adapter) when adapter in @adapters, + do: ["integration_test/#{adapter}"] + + defp test_paths(nil), do: ["test"] + defp test_paths(other), do: raise("unknown adapter #{inspect(other)}") + + defp test_adapters(args) do + for adapter <- @adapters do + IO.puts("==> Running tests for ECTO_ADAPTER=#{adapter} mix test") + + {_, res} = + System.cmd("mix", ["test", ansi_option() | args], + into: IO.binstream(:stdio, :line), + env: [{"ECTO_ADAPTER", adapter}] + ) + + if res > 0 do + System.at_exit(fn _ -> exit({:shutdown, 1}) end) + end + end + end + + defp ansi_option do + if IO.ANSI.enabled?(), do: "--color", else: "--no-color" + end end diff --git a/test/flop_test.exs b/test/flop_test.exs index 701ebe89..6edc124d 100644 --- a/test/flop_test.exs +++ b/test/flop_test.exs @@ -1,1722 +1,18 @@ defmodule FlopTest do use ExUnit.Case, async: true - use ExUnitProperties - - doctest Flop, import: true import Ecto.Query - import Flop.Factory - import Flop.Generators - import Flop.TestUtil alias __MODULE__.TestProvider - alias Ecto.Adapters.SQL.Sandbox - alias Ecto.Query.QueryExpr - alias Flop.Filter alias Flop.Meta - alias Flop.Repo alias MyApp.Fruit - alias MyApp.Owner alias MyApp.Pet alias MyApp.Vegetable - @pet_count_range 1..200 - - setup do - :ok = Sandbox.checkout(Repo) - end - defmodule TestProvider do use Flop, repo: Flop.Repo, default_limit: 35 end - defmodule TestProviderNested do - use Flop, - adapter_opts: [repo: Flop.Repo], - default_limit: 35 - end - - describe "ordering" do - test "adds order_by to query if set" do - pets = insert_list(20, :pet) - - expected = - Enum.sort( - pets, - &(&1.species < &2.species || - (&1.species == &2.species && &1.name >= &2.name)) - ) - - assert Flop.all(Pet, %Flop{ - order_by: [:species, :name], - order_directions: [:asc, :desc] - }) == expected - end - - test "uses :asc as default direction if no directions are passed" do - pets = insert_list(20, :pet) - expected = Enum.sort_by(pets, &{&1.species, &1.name, &1.age}) - - assert Flop.all(Pet, %Flop{ - order_by: [:species, :name, :age], - order_directions: nil - }) == expected - end - - test "uses :asc as default direction if not enough directions are passed" do - pets = insert_list(20, :pet) - - expected = - Enum.sort( - pets, - &(&1.species > &2.species || - (&1.species == &2.species && - (&1.name < &2.name || - (&1.name == &2.name && &1.age <= &2.age)))) - ) - - assert Flop.all(Pet, %Flop{ - order_by: [:species, :name, :age], - order_directions: [:desc] - }) == expected - end - - test "orders by join fields" do - pets = insert_list(20, :pet_with_owner) - - expected = - Enum.sort_by( - pets, - &{&1.owner.name, &1.owner.age, &1.name, &1.age} - ) - - result = - Pet - |> join(:left, [p], o in assoc(p, :owner), as: :owner) - |> preload(:owner) - |> Flop.all( - %Flop{order_by: [:owner_name, :owner_age, :name, :age]}, - for: Pet - ) - - assert result == expected - end - - test "orders by compound fields" do - pets = insert_list(20, :pet) - - expected = - Enum.sort_by( - pets, - &{&1.family_name, &1.given_name, &1.id} - ) - - result = Flop.all(Pet, %Flop{order_by: [:full_name, :id]}, for: Pet) - assert result == expected - end - - test "orders by compound fields with join fields" do - pets = insert_list(20, :pet, owner: fn -> build(:owner) end) - - expected = - pets |> Enum.map(&{&1.name, &1.owner.name, &1.id}) |> Enum.sort() - - q = - Pet - |> join(:left, [p], o in assoc(p, :owner), as: :owner) - |> select([p, owner: o], {p.name, o.name, p.id}) - - assert Flop.all( - q, - %Flop{order_by: [:pet_and_owner_name, :id]}, - for: Pet - ) == expected - - assert Flop.all( - q, - %Flop{ - order_by: [:pet_and_owner_name, :id], - order_directions: [:desc, :desc] - }, - for: Pet - ) == Enum.reverse(expected) - end - - test "orders by alias fields" do - owner_1 = insert(:owner, pets: build_list(2, :pet)) - owner_2 = insert(:owner, pets: build_list(1, :pet)) - owner_3 = insert(:owner, pets: build_list(4, :pet)) - owner_4 = insert(:owner, pets: build_list(3, :pet)) - - expected = [ - {owner_2.id, 1}, - {owner_1.id, 2}, - {owner_4.id, 3}, - {owner_3.id, 4} - ] - - q = - Owner - |> join(:left, [o], p in assoc(o, :pets), as: :pets) - |> group_by([o], o.id) - |> select( - [o, pets: p], - {o.id, p.id |> count() |> selected_as(:pet_count)} - ) - - assert Flop.all(q, %Flop{order_by: [:pet_count]}, for: Owner) == expected - - assert Flop.all( - q, - %Flop{order_by: [:pet_count], order_directions: [:desc]}, - for: Owner - ) == Enum.reverse(expected) - end - end - - describe "filtering" do - property "applies equality filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - # all except compound fields - field <- - member_of([:age, :name, :owner_age, :owner_name, :species]), - pet <- member_of(pets), - query_value <- pet |> Pet.get_field(field) |> constant(), - query_value != "" do - expected = filter_items(pets, field, :==, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :==, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - test "does not allow equality filter on compound fields" do - assert_raise Flop.InvalidParamsError, fn -> - query_pets_with_owners(%{ - filters: [%{field: :full_name, op: :==, value: "o"}] - }) - end - end - - property "applies inequality filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - # all except compound fields - field <- - member_of([:age, :name, :owner_age, :owner_name, :species]), - pet <- member_of(pets), - query_value = Pet.get_field(pet, field), - query_value != "" do - expected = filter_items(pets, field, :!=, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :!=, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - test "does not allow inequality filter on compound fields" do - assert_raise Flop.InvalidParamsError, fn -> - query_pets_with_owners(%{ - filters: [%{field: :full_name, op: :!=, value: "o"}] - }) - end - end - - test "applies empty and not_empty filter" do - check all pet_count <- integer(@pet_count_range), - pets = - insert_list_and_sort(pet_count, :pet, - species: fn -> Enum.random([nil, "fox"]) end, - owner: fn -> - build(:owner, name: fn -> Enum.random([nil, "Carl"]) end) - end - ), - field <- member_of([:species, :owner_name]), - op <- member_of([:empty, :not_empty]) do - [opposite_op] = [:empty, :not_empty] -- [op] - expected = filter_items(pets, field, op) - opposite_expected = filter_items(pets, field, opposite_op) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: true}] - }) == expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: false}] - }) == opposite_expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: true}] - }) == opposite_expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: false}] - }) == expected - - checkin_checkout() - end - end - - test "applies empty and not_empty filter with string values" do - check all pet_count <- integer(@pet_count_range), - pets = - insert_list_and_sort(pet_count, :pet, - species: fn -> Enum.random([nil, "fox"]) end, - owner: fn -> - build(:owner, name: fn -> Enum.random([nil, "Carl"]) end) - end - ), - field <- member_of([:species, :owner_name]), - op <- member_of([:empty, :not_empty]) do - [opposite_op] = [:empty, :not_empty] -- [op] - expected = filter_items(pets, field, op) - opposite_expected = filter_items(pets, field, opposite_op) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: "true"}] - }) == expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: "false"}] - }) == opposite_expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: "true"}] - }) == opposite_expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: "false"}] - }) == expected - - checkin_checkout() - end - end - - test "applies empty and not_empty filter to array fields" do - check all pet_count <- integer(@pet_count_range), - pets = - insert_list_and_sort(pet_count, :pet_with_owner, - tags: fn -> Enum.random([nil, [], ["catdog"]]) end, - owner: fn -> - build(:owner, - tags: fn -> Enum.random([nil, [], ["catlover"]]) end - ) - end - ), - field <- member_of([:tags, :owner_tags]), - op <- member_of([:empty, :not_empty]) do - [opposite_op] = [:empty, :not_empty] -- [op] - expected = filter_items(pets, field, op, true) - opposite_expected = filter_items(pets, field, opposite_op, true) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: true}] - }) == expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: false}] - }) == opposite_expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: true}] - }) == opposite_expected - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: false}] - }) == expected - - checkin_checkout() - end - end - - test "applies empty and not_empty filter to map fields" do - check all fruit_count <- integer(@pet_count_range), - fruits = - insert_list_and_sort(fruit_count, :fruit, - attributes: fn -> Enum.random([nil, %{}, %{"a" => "b"}]) end, - extra: fn -> Enum.random([nil, %{}, %{"a" => "b"}]) end, - owner: - build(:owner, - attributes: fn -> - Enum.random([nil, %{}, %{"a" => "b"}]) - end, - extra: fn -> Enum.random([nil, %{}, %{"a" => "b"}]) end - ) - ), - field <- - member_of([ - :attributes, - :extra, - :owner_attributes, - :owner_extra - ]), - op <- member_of([:empty, :not_empty]) do - [opposite_op] = [:empty, :not_empty] -- [op] - expected = filter_items(fruits, field, op, true) - opposite_expected = filter_items(fruits, field, opposite_op, true) - - assert query_fruits_with_owners(%{ - filters: [%{field: field, op: op, value: true}] - }) == expected - - assert query_fruits_with_owners(%{ - filters: [%{field: field, op: op, value: false}] - }) == opposite_expected - - assert query_fruits_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: true}] - }) == opposite_expected - - assert query_fruits_with_owners(%{ - filters: [%{field: field, op: opposite_op, value: false}] - }) == expected - - checkin_checkout() - end - end - - property "applies like filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - query_value <- substring(value) do - expected = filter_items(pets, field, :like, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :like, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - test "escapes % in (i)like queries" do - %{id: _id1} = insert(:pet, name: "abc") - %{id: id2} = insert(:pet, name: "a%c") - - for op <- [:like, :ilike, :like_and, :like_or, :ilike_and, :ilike_or] do - flop = %Flop{filters: [%Filter{field: :name, op: op, value: "a%c"}]} - assert [%Pet{id: ^id2}] = Flop.all(Pet, flop) - end - end - - test "escapes _ in (i)like queries" do - %{id: _id1} = insert(:pet, name: "abc") - %{id: id2} = insert(:pet, name: "a_c") - - for op <- [:like, :ilike, :like_and, :like_or, :ilike_and, :ilike_or] do - flop = %Flop{filters: [%Filter{field: :name, op: op, value: "a_c"}]} - assert [%Pet{id: ^id2}] = Flop.all(Pet, flop) - end - end - - test "escapes \\ in (i)like queries" do - %{id: _id1} = insert(:pet, name: "abc") - %{id: id2} = insert(:pet, name: "a\\c") - - for op <- [:like, :ilike, :like_and, :like_or, :ilike_and, :ilike_or] do - flop = %Flop{filters: [%Filter{field: :name, op: op, value: "a\\c"}]} - assert [%Pet{id: ^id2}] = Flop.all(Pet, flop) - end - end - - property "applies not like filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - query_value <- substring(value) do - expected = filter_items(pets, field, :not_like, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :not_like, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies ilike filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - op <- member_of([:=~, :ilike]), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - query_value <- substring(value) do - expected = filter_items(pets, field, :ilike, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies not ilike filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - query_value <- substring(value) do - expected = filter_items(pets, field, :not_ilike, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :not_ilike, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies like_and filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - search_text_or_list <- search_text_or_list(value) do - expected = filter_items(pets, field, :like_and, search_text_or_list) - - assert query_pets_with_owners(%{ - filters: [ - %{field: field, op: :like_and, value: search_text_or_list} - ] - }) == expected - - checkin_checkout() - end - end - - property "applies like_or filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - search_text_or_list <- search_text_or_list(value) do - expected = filter_items(pets, field, :like_or, search_text_or_list) - - assert query_pets_with_owners(%{ - filters: [ - %{field: field, op: :like_or, value: search_text_or_list} - ] - }) == expected - - checkin_checkout() - end - end - - property "applies ilike_and filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - search_text_or_list <- search_text_or_list(value) do - expected = filter_items(pets, field, :ilike_and, search_text_or_list) - - assert query_pets_with_owners(%{ - filters: [ - %{field: field, op: :ilike_and, value: search_text_or_list} - ] - }) == expected - - checkin_checkout() - end - end - - property "applies ilike_or filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- filterable_pet_field(:string), - pet <- member_of(pets), - value = Pet.get_field(pet, field), - search_text_or_list <- search_text_or_list(value) do - expected = filter_items(pets, field, :ilike_or, search_text_or_list) - - assert query_pets_with_owners(%{ - filters: [ - %{field: field, op: :ilike_or, value: search_text_or_list} - ] - }) == expected - - checkin_checkout() - end - end - - property "applies lte, lt, gt and gte filters" do - check all pet_count <- integer(@pet_count_range), - pets = - pet_count - |> insert_list(:pet_downcase, owner: fn -> build(:owner) end) - |> Enum.sort_by(& &1.id), - field <- member_of([:age, :name, :owner_age]), - op <- one_of([:<=, :<, :>, :>=]), - query_value <- compare_value_by_field(field) do - expected = filter_items(pets, field, op, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: op, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies :in operator" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- member_of([:age, :name, :owner_age]), - values = Enum.map(pets, &Map.get(&1, field)), - query_value <- - list_of(one_of([member_of(values), value_by_field(field)]), - max_length: 5 - ) do - expected = filter_items(pets, field, :in, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :in, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies :not_in operator" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- member_of([:age, :name, :owner_age]), - values = Enum.map(pets, &Map.get(&1, field)), - query_value <- - list_of(one_of([member_of(values), value_by_field(field)]), - max_length: 5 - ) do - expected = filter_items(pets, field, :not_in, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :not_in, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies :contains operator" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- member_of([:tags, :owner_tags]), - values = Enum.flat_map(pets, &Pet.get_field(&1, field)), - query_value <- member_of(values) do - expected = filter_items(pets, field, :contains, query_value) - - assert query_pets_with_owners(%{ - filters: [%{field: field, op: :contains, value: query_value}] - }) == expected - - checkin_checkout() - end - end - - property "applies :not_contains operator" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - field <- member_of([:tags, :owner_tags]), - values = Enum.flat_map(pets, &Pet.get_field(&1, field)), - query_value <- member_of(values) do - expected = filter_items(pets, field, :not_contains, query_value) - - assert query_pets_with_owners(%{ - filters: [ - %{field: field, op: :not_contains, value: query_value} - ] - }) == expected - - checkin_checkout() - end - end - - property "custom field filter" do - check all pet_count <- integer(@pet_count_range), - pets = insert_list_and_sort(pet_count, :pet_with_owner), - values = Enum.map(pets, &String.reverse(&1.name)), - query_value <- member_of(values) do - expected = filter_items(pets, :name, :==, query_value) - - assert query_pets_with_owners(%{ - filters: [ - %{field: :reverse_name, op: :==, value: query_value} - ] - }) == expected - - checkin_checkout() - end - end - - test "filtering with custom fields" do - pets = insert_list_and_sort(1, :pet_with_owner) - - assert [] = - query_pets_with_owners(%{ - filters: [ - %{field: :custom, op: :==, value: "some_value"} - ] - }) - - receive do - {:filter, filter} -> - assert filter == - {%Flop.Filter{field: :custom, op: :==, value: "some_value"}, - [some: :options]} - end - - assert ^pets = - query_pets_with_owners( - %{ - filters: [ - %{field: :custom, op: :==, value: "some_other_value"} - ] - }, - extra_opts: [other: :options] - ) - - receive do - {:filter, filter} -> - assert filter == - {%Flop.Filter{ - field: :custom, - op: :==, - value: "some_other_value" - }, [other: :options, some: :options]} - end - end - - test "silently ignores nil values for field and value" do - flop = %Flop{filters: [%Filter{op: :>=, value: 4}]} - assert Flop.query(Pet, flop) == Pet - - flop = %Flop{filters: [%Filter{field: :name, op: :>=}]} - assert Flop.query(Pet, flop) == Pet - end - - test "leaves query unchanged if everything is nil" do - flop = %Flop{ - filters: nil, - limit: nil, - offset: nil, - order_by: nil, - order_directions: nil, - page: nil, - page_size: nil - } - - assert Flop.query(Pet, flop) == Pet - end - end - - describe "all/3" do - test "returns all matching entries" do - matching_pets = insert_list(6, :pet, age: 5) - _non_matching_pets = insert_list(4, :pet, age: 6) - - [_, _, %{name: name_1}, %{name: name_2}, _, _] = - Enum.sort_by(matching_pets, & &1.name) - - flop = %Flop{ - limit: 2, - offset: 2, - order_by: [:name], - filters: [%Filter{field: :age, op: :<=, value: 5}] - } - - assert Enum.map(Flop.all(Pet, flop), & &1.name) == [name_1, name_2] - end - - test "can apply a query prefix" do - insert(:pet, %{}, prefix: "other_schema") - - assert Flop.all(Pet, %Flop{}) == [] - refute Flop.all(Pet, %Flop{}, query_opts: [prefix: "other_schema"]) == [] - end - end - - describe "count/3" do - test "returns count of matching entries" do - _matching_pets = insert_list(6, :pet, age: 5) - _non_matching_pets = insert_list(4, :pet, age: 6) - - flop = %Flop{ - limit: 2, - offset: 2, - order_by: [:age], - filters: [%Filter{field: :age, op: :<=, value: 5}] - } - - assert Flop.count(Pet, flop) == 6 - end - - test "can apply a query prefix" do - insert(:pet, %{}, prefix: "other_schema") - - assert Flop.count(Pet, %Flop{}) == 0 - assert Flop.count(Pet, %Flop{}, query_opts: [prefix: "other_schema"]) == 1 - end - - test "allows overriding query" do - _matching_pets = insert_list(6, :pet, age: 5, name: "A") - _more_matching_pets = insert_list(5, :pet, age: 5, name: "B") - _non_matching_pets = insert_list(4, :pet, age: 6) - - flop = %Flop{ - limit: 2, - offset: 2, - order_by: [:age], - filters: [%Filter{field: :age, op: :<=, value: 5}] - } - - # default query - assert Flop.count(Pet, flop) == 11 - - # custom count query - assert Flop.count(Pet, flop, count_query: where(Pet, name: "A")) == 6 - assert Flop.count(Pet, flop, count_query: where(Pet, name: "B")) == 5 - end - - test "allows overriding the count itself" do - _matching_pets = insert_list(6, :pet, age: 5, name: "A") - _more_matching_pets = insert_list(5, :pet, age: 5, name: "B") - _non_matching_pets = insert_list(4, :pet, age: 6) - - flop = %Flop{ - limit: 2, - offset: 2, - order_by: [:age], - filters: [%Filter{field: :age, op: :<=, value: 5}] - } - - # default query - assert Flop.count(Pet, flop) == 11 - - # custom count - assert Flop.count(Pet, flop, count: 6) == 6 - end - - test "counts entries for queries with group_by clauses" do - _owner_1 = insert(:owner, age: 13, pets: build_list(3, :pet)) - _owner_2 = insert(:owner, age: 20, pets: build_list(2, :pet)) - _owner_3 = insert(:owner, age: 22, pets: build_list(1, :pet)) - _non_matching_owner = insert(:owner, age: 52, pets: build_list(4, :pet)) - - q = - Owner - |> join(:left, [o], p in assoc(o, :pets), as: :pets) - |> group_by([o], o.id) - |> select( - [o, pets: p], - %{o | pet_count: count(p.id)} - ) - - flop = %Flop{ - filters: [%Filter{field: :age, op: :<=, value: 30}] - } - - assert Flop.count(q, flop) == 3 - end - end - - describe "meta/3" do - test "returns the meta information for a query with limit/offset" do - _matching_pets = insert_list(7, :pet, age: 5) - _non_matching_pets = insert_list(4, :pet, age: 6) - - flop = %Flop{ - limit: 2, - offset: 4, - filters: [%Filter{field: :age, op: :<=, value: 5}] - } - - assert Flop.meta(Pet, flop) == %Meta{ - current_offset: 4, - current_page: 3, - end_cursor: nil, - flop: flop, - has_next_page?: true, - has_previous_page?: true, - next_offset: 6, - next_page: 4, - page_size: 2, - previous_offset: 2, - previous_page: 2, - start_cursor: nil, - total_count: 7, - total_pages: 4 - } - end - - test "returns the meta information for a query with page/page_size" do - _matching_pets = insert_list(7, :pet, age: 5) - _non_matching_pets = insert_list(4, :pet, age: 6) - - flop = %Flop{ - page_size: 2, - page: 3, - filters: [%Filter{field: :age, op: :<=, value: 5}] - } - - assert Flop.meta(Pet, flop) == %Meta{ - current_offset: 4, - current_page: 3, - end_cursor: nil, - flop: flop, - has_next_page?: true, - has_previous_page?: true, - next_offset: 6, - next_page: 4, - page_size: 2, - previous_offset: 2, - previous_page: 2, - start_cursor: nil, - total_count: 7, - total_pages: 4 - } - end - - test "returns the meta information for a query without limit" do - _matching_pets = insert_list(7, :pet, age: 5) - _non_matching_pets = insert_list(2, :pet, age: 6) - - flop = %Flop{filters: [%Filter{field: :age, op: :<=, value: 5}]} - - assert Flop.meta(Pet, flop) == %Meta{ - current_offset: 0, - current_page: 1, - end_cursor: nil, - flop: flop, - has_next_page?: false, - has_previous_page?: false, - next_offset: nil, - next_page: nil, - page_size: nil, - previous_offset: nil, - previous_page: nil, - start_cursor: nil, - total_count: 7, - total_pages: 1 - } - end - - test "rounds current page if offset is between pages" do - insert_list(6, :pet) - - assert %Meta{ - current_offset: 1, - current_page: 2, - has_next_page?: true, - has_previous_page?: true, - next_offset: 3, - next_page: 3, - previous_offset: 0, - previous_page: 1 - } = Flop.meta(Pet, %Flop{limit: 2, offset: 1}) - - assert %Meta{ - current_offset: 3, - current_page: 3, - has_next_page?: true, - has_previous_page?: true, - next_offset: 5, - next_page: 3, - previous_offset: 1, - previous_page: 2 - } = Flop.meta(Pet, %Flop{limit: 2, offset: 3}) - - # current page shouldn't be greater than total page numbers - assert %Meta{ - current_offset: 5, - current_page: 3, - has_next_page?: false, - has_previous_page?: true, - next_offset: nil, - next_page: nil, - previous_offset: 3, - previous_page: 2 - } = Flop.meta(Pet, %Flop{limit: 2, offset: 5}) - end - - test "sets has_previous_page? and has_next_page?" do - _matching_pets = insert_list(5, :pet) - - assert %Meta{has_next_page?: true, has_previous_page?: false} = - Flop.meta(Pet, %Flop{limit: 2, offset: 0}) - - assert %Meta{has_next_page?: true, has_previous_page?: true} = - Flop.meta(Pet, %Flop{limit: 2, offset: 1}) - - assert %Meta{has_next_page?: true, has_previous_page?: true} = - Flop.meta(Pet, %Flop{limit: 2, offset: 2}) - - assert %Meta{has_next_page?: false, has_previous_page?: true} = - Flop.meta(Pet, %Flop{limit: 2, offset: 3}) - - assert %Meta{has_next_page?: false, has_previous_page?: true} = - Flop.meta(Pet, %Flop{limit: 2, offset: 4}) - - assert %Meta{has_next_page?: true, has_previous_page?: false} = - Flop.meta(Pet, %Flop{page_size: 3, page: 1}) - - assert %Meta{has_next_page?: false, has_previous_page?: true} = - Flop.meta(Pet, %Flop{page_size: 3, page: 2}) - end - - test "can apply a query prefix" do - insert(:pet, %{}, prefix: "other_schema") - - assert Flop.meta(Pet, %Flop{}).total_count == 0 - - assert Flop.meta( - Pet, - %Flop{}, - query_opts: [prefix: "other_schema"] - ).total_count == 1 - end - - test "sets the schema if :for option is passed" do - assert Flop.meta(Pet, %Flop{}).schema == nil - assert Flop.meta(Pet, %Flop{}, for: Pet).schema == Pet - end - - test "sets options" do - opts = Flop.meta(Pet, %Flop{}, for: Pet).opts - assert opts[:for] == Pet - end - end - - describe "run/3" do - test "returns data and meta data" do - insert_list(3, :pet) - flop = %Flop{page_size: 2, page: 2} - assert {[%Pet{}], %Meta{}} = Flop.run(Pet, flop) - end - end - - describe "validate_and_run/3" do - test "returns error if flop is invalid" do - flop = %Flop{ - page_size: -1, - filters: [%Filter{field: :name, op: :something_like}] - } - - assert {:error, %Meta{} = meta} = Flop.validate_and_run(Pet, flop) - - assert meta.params == %{ - "page_size" => -1, - "filters" => [%{"field" => :name, "op" => :something_like}] - } - - assert [filters: [_], page_size: [_]] = meta.errors - end - - test "returns data and meta data" do - insert_list(3, :pet) - flop = %{page_size: 2, page: 2} - assert {:ok, {[%Pet{}], %Meta{}}} = Flop.validate_and_run(Pet, flop) - end - end - - describe "validate_and_run!/3" do - test "raises if flop is invalid" do - assert_raise Flop.InvalidParamsError, fn -> - Flop.validate_and_run!(Pet, %{limit: -1}) - end - end - - test "returns data and meta data" do - insert_list(3, :pet) - flop = %{page_size: 2, page: 2} - assert {[%Pet{}], %Meta{}} = Flop.validate_and_run!(Pet, flop) - end - end - - describe "offset-based pagination" do - test "applies limit to query" do - insert_list(6, :pet) - assert Pet |> Flop.query(%Flop{limit: 4}) |> Repo.all() |> length() == 4 - end - - test "applies offset to query if set" do - pets = insert_list(10, :pet) - - expected_pets = - pets - |> Enum.sort_by(&{&1.name, &1.species, &1.age}) - |> Enum.slice(4..10) - - flop = %Flop{offset: 4, order_by: [:name, :species, :age]} - query = Flop.query(Pet, flop) - assert %QueryExpr{params: [{4, :integer}]} = query.offset - assert Repo.all(query) == expected_pets - end - - test "applies limit and offset to query if page and page size are set" do - pets = insert_list(40, :pet) - sorted_pets = Enum.sort_by(pets, &{&1.name, &1.species, &1.age}) - order_by = [:name, :species, :age] - - flop = %Flop{page: 1, page_size: 10, order_by: order_by} - query = Flop.query(Pet, flop) - assert %QueryExpr{params: [{0, :integer}]} = query.offset - assert %{params: [{10, :integer}]} = query.limit - assert Repo.all(query) == Enum.slice(sorted_pets, 0..9) - - flop = %Flop{page: 2, page_size: 10, order_by: order_by} - query = Flop.query(Pet, flop) - assert %QueryExpr{params: [{10, :integer}]} = query.offset - assert %{params: [{10, :integer}]} = query.limit - assert Repo.all(query) == Enum.slice(sorted_pets, 10..19) - - flop = %Flop{page: 3, page_size: 4, order_by: order_by} - query = Flop.query(Pet, flop) - assert %QueryExpr{params: [{8, :integer}]} = query.offset - assert %{params: [{4, :integer}]} = query.limit - assert Repo.all(query) == Enum.slice(sorted_pets, 8..11) - end - end - - describe "cursor pagination" do - property "querying cursor by cursor forward includes all items in order" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}) do - checkin_checkout() - - # insert pets into DB, retrieve them so we have the IDs - Enum.each(pets, &Repo.insert!(&1)) - - pets = - Flop.all( - pets_with_owners_query(), - %Flop{order_by: cursor_fields, order_directions: directions}, - for: Pet - ) - - # retrieve first cursor, ensure returned pet matches first one in list - [first_pet | remaining_pets] = pets - - {:ok, {[returned_pet], %Meta{end_cursor: cursor}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - assert returned_pet == first_pet - - # iterate over remaining pets, query DB cursor by cursor - {reversed_returned_pets, last_cursor} = - Enum.reduce( - remaining_pets, - {[first_pet], cursor}, - fn _current_pet, {pet_list, cursor} -> - assert {:ok, - {[returned_pet], - %Meta{ - end_cursor: new_cursor, - flop: %Flop{decoded_cursor: nil} - }}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - after: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - {[returned_pet | pet_list], new_cursor} - end - ) - - # ensure the accumulated list matches the manually sorted list - returned_pets = Enum.reverse(reversed_returned_pets) - - assert returned_pets == pets - - # ensure nothing comes after the last cursor - assert {:ok, {[], %Meta{end_cursor: nil}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - after: last_cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "querying all items returns same list forward and backward" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - pet_count = length(pets) - - {:ok, {with_first, _meta}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: pet_count, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - {:ok, {with_last, _meta}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - last: pet_count, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - assert with_first == with_last - end - end - - property "querying cursor by cursor backward includes all items in order" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}) do - checkin_checkout() - - # insert pets into DB, retrieve them so we have the IDs - Enum.each(pets, &Repo.insert!(&1)) - - pets = - Flop.all( - pets_with_owners_query(), - %Flop{order_by: cursor_fields, order_directions: directions}, - for: Pet - ) - - pets = Enum.reverse(pets) - - # retrieve last cursor, ensure returned pet matches last one in list - [last_pet | remaining_pets] = pets - - {:ok, {[returned_pet], %Meta{end_cursor: cursor}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - last: 1, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - assert returned_pet == last_pet - - # iterate over remaining pets, query DB cursor by cursor - {reversed_returned_pets, last_cursor} = - Enum.reduce( - remaining_pets, - {[last_pet], cursor}, - fn _current_pet, {pet_list, cursor} -> - assert {:ok, {[returned_pet], %Meta{end_cursor: new_cursor}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - last: 1, - before: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - {[returned_pet | pet_list], new_cursor} - end - ) - - # ensure the accumulated list matches the manually sorted list - returned_pets = Enum.reverse(reversed_returned_pets) - assert returned_pets == pets - - # ensure nothing comes after the last cursor - assert {:ok, {[], %Meta{end_cursor: nil}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - last: 1, - before: last_cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_previous_page? is false without after and last" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - first <- integer(1..(length(pets) + 1)) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - - assert {_, %Meta{has_previous_page?: false}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - first: first, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_previous_page? is true with after" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - first <- integer(1..(length(pets) + 1)), - cursor_pet <- member_of(pets) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - - cursor = - cursor_fields - |> Enum.into(%{}, fn field -> - {field, Pet.get_field(cursor_pet, field)} - end) - |> Flop.Cursor.encode() - - assert {_, %Meta{has_previous_page?: true}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - first: first, - after: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_previous_page? is true with last set and items left" do - check all pets <- uniq_list_of_pets(length: 3..50), - pet_count = length(pets), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - last <- integer(1..(pet_count - 2)), - cursor_index <- integer((last + 1)..(pet_count - 1)) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - - # retrieve ordered pets - pets = - Flop.all( - pets_with_owners_query(), - %Flop{order_by: cursor_fields, order_directions: directions}, - for: Pet - ) - - # retrieve cursor - pet = Enum.at(pets, cursor_index) - - cursor = - cursor_fields - |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) - |> Flop.Cursor.encode() - - assert {_, %Meta{has_previous_page?: true}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - last: last, - before: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_previous_page? is false with last set and no items left" do - check all pets <- uniq_list_of_pets(length: 3..50), - pet_count = length(pets), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - # include test with limits greater than item count - last <- integer(1..(pet_count + 20)), - cursor_index <- integer(0..min(pet_count - 1, last)) do - checkin_checkout() - - # insert pets - Enum.each(pets, &Repo.insert!(&1)) - - # retrieve ordered pets - pets = - Flop.all( - pets_with_owners_query(), - %Flop{order_by: cursor_fields, order_directions: directions}, - for: Pet - ) - - # retrieve cursor - pet = Enum.at(pets, cursor_index) - - cursor = - cursor_fields - |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) - |> Flop.Cursor.encode() - - assert {_, %Meta{has_previous_page?: false}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - last: last, - before: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_next_page? is false without first and before" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - last <- integer(1..(length(pets) + 1)) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - - assert {_, %Meta{has_next_page?: false}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - last: last, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_next_page? is true with before" do - check all pets <- uniq_list_of_pets(length: 1..25), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - last <- integer(1..(length(pets) + 1)), - cursor_pet <- member_of(pets) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - - cursor = - cursor_fields - |> Enum.into(%{}, fn field -> - {field, Pet.get_field(cursor_pet, field)} - end) - |> Flop.Cursor.encode() - - assert {_, %Meta{has_next_page?: true}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - last: last, - before: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_next_page? is true with first set and items left" do - check all pets <- uniq_list_of_pets(length: 3..50), - pet_count = length(pets), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - first <- integer(1..(pet_count - 2)), - cursor_index <- integer((first + 1)..(pet_count - 1)) do - checkin_checkout() - Enum.each(pets, &Repo.insert!(&1)) - - # retrieve ordered pets - pets = - pets_with_owners_query() - |> Flop.all( - %Flop{order_by: cursor_fields, order_directions: directions}, - for: Pet - ) - |> Enum.reverse() - - # retrieve cursor - pet = Enum.at(pets, cursor_index) - - cursor = - cursor_fields - |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) - |> Flop.Cursor.encode() - - assert {_, %Meta{has_next_page?: true}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - first: first, - after: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - property "has_next_page? is false with first set and no items left" do - check all pet_count <- integer(3..50), - pets <- uniq_list_of_pets(length: pet_count..pet_count), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}), - # include test with limits greater than item count - first <- integer(1..(pet_count + 20)), - cursor_index <- - integer(max(0, pet_count - first)..(pet_count - 1)) do - checkin_checkout() - - Enum.each(pets, &Repo.insert!(&1)) - - # retrieve ordered pets - pets = - Flop.all( - pets_with_owners_query(), - %Flop{order_by: cursor_fields, order_directions: directions}, - for: Pet - ) - - # retrieve cursor - pet = Enum.at(pets, cursor_index) - - cursor = - cursor_fields - |> Enum.into(%{}, fn field -> {field, Pet.get_field(pet, field)} end) - |> Flop.Cursor.encode() - - assert {_, %Meta{has_next_page?: false}} = - Flop.validate_and_run!( - pets_with_owners_query(), - %Flop{ - first: first, - after: cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - test "cursor value function can be overridden" do - insert_list(4, :pet) - query = select(Pet, [p], {p, %{other: :data}}) - - cursor_value_func = fn {pet, _}, order_by -> - Map.take(pet, order_by) - end - - {:ok, - {_r1, - %Meta{ - end_cursor: end_cursor, - has_next_page?: true - }}} = - Flop.validate_and_run( - query, - %Flop{first: 2, order_by: [:id]}, - cursor_value_func: cursor_value_func - ) - - {:ok, - {_r2, - %Meta{ - end_cursor: _end_cursor, - has_next_page?: false - }}} = - Flop.validate_and_run( - query, - %Flop{first: 2, after: end_cursor, order_by: [:id]}, - cursor_value_func: cursor_value_func - ) - end - - test "nil values for cursors are ignored when using for option" do - check all pets <- uniq_list_of_pets(length: 2..2), - cursor_fields <- cursor_fields(%Pet{}), - directions <- order_directions(%Pet{}) do - checkin_checkout() - - # set name fields to nil and insert - pets - |> Enum.map(&Map.update!(&1, :name, fn _ -> nil end)) - |> Enum.each(&Repo.insert!(&1)) - - assert {:ok, {[_], %Meta{end_cursor: end_cursor}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - - assert {:ok, _} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - after: end_cursor, - order_by: cursor_fields, - order_directions: directions - }, - for: Pet - ) - end - end - - test "raises if alias field is used" do - q = - Owner - |> join(:left, [o], p in assoc(o, :pets), as: :pets) - |> group_by([o], o.id) - |> select( - [o, pets: p], - %{o | pet_count: p.id |> count() |> selected_as(:pet_count)} - ) - - insert(:owner) - - assert {_, %Meta{end_cursor: end_cursor}} = - Flop.run( - q, - %Flop{first: 1, order_by: [:pet_count, :id]}, - for: Owner - ) - - error = - assert_raise RuntimeError, - fn -> - Flop.run( - q, - %Flop{ - first: 1, - after: end_cursor, - order_by: [:pet_count, :id] - }, - for: Owner - ) - end - - assert error.message =~ - "alias fields are not supported in cursor pagination" - end - - test "nil values for cursors are ignored when not using for option" do - check all pets <- uniq_list_of_pets(length: 2..2), - directions <- order_directions(%Pet{}) do - checkin_checkout() - cursor_fields = [:name, :age] - - # set name fields to nil and insert - pets - |> Enum.map(&Map.update!(&1, :name, fn _ -> nil end)) - |> Enum.each(&Repo.insert!(&1)) - - assert {:ok, {[_], %Meta{end_cursor: end_cursor}}} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - order_by: cursor_fields, - order_directions: directions - } - ) - - assert {:ok, _} = - Flop.validate_and_run( - pets_with_owners_query(), - %Flop{ - first: 1, - after: end_cursor, - order_by: cursor_fields, - order_directions: directions - } - ) - end - end - - test "paging on composite type" do - 1..10 - |> Enum.map( - &MyApp.WalkingDistances.changeset(%MyApp.WalkingDistances{}, %{ - trip: %{value: &1, unit: "m"} - }) - ) - |> Enum.each(&Repo.insert!(&1)) - - assert {:ok, {[_element], %Meta{end_cursor: end_cursor}}} = - Flop.validate_and_run( - MyApp.WalkingDistances, - %Flop{first: 1, order_by: [:trip]}, - for: MyApp.WalkingDistances - ) - - assert {:ok, {[_element], %Meta{}}} = - Flop.validate_and_run( - MyApp.WalkingDistances, - %Flop{first: 1, after: end_cursor, order_by: [:trip]}, - for: MyApp.WalkingDistances - ) - end - end - describe "validate/1" do test "returns Flop struct" do assert Flop.validate(%Flop{}) == {:ok, %Flop{limit: 50}} @@ -1983,52 +279,6 @@ defmodule FlopTest do end end - describe "__using__/1" do - test "defines wrapper functions that pass default options" do - insert_list(3, :pet) - - assert {:ok, {_, %Meta{page_size: 35}}} = - TestProvider.validate_and_run(Pet, %{}) - end - - test "allows to override defaults" do - insert_list(3, :pet) - - assert {:ok, {_, %Meta{page_size: 30}}} = - TestProvider.validate_and_run(Pet, %{page_size: 30}) - end - - test "passes backend module" do - assert {:ok, {_, %Meta{backend: TestProvider, opts: opts}}} = - TestProvider.validate_and_run(Pet, %{}) - - assert Keyword.get(opts, :backend) == TestProvider - end - end - - describe "__using__/1 with nested adapter options" do - test "defines wrapper functions that pass default options" do - insert_list(3, :pet) - - assert {:ok, {_, %Meta{page_size: 35}}} = - TestProviderNested.validate_and_run(Pet, %{}) - end - - test "allows to override defaults" do - insert_list(3, :pet) - - assert {:ok, {_, %Meta{page_size: 30}}} = - TestProviderNested.validate_and_run(Pet, %{page_size: 30}) - end - - test "passes backend module" do - assert {:ok, {_, %Meta{backend: TestProviderNested, opts: opts}}} = - TestProviderNested.validate_and_run(Pet, %{}) - - assert Keyword.get(opts, :backend) == TestProviderNested - end - end - describe "get_option/3" do test "returns value from option list" do # sanity check diff --git a/test/support/repo.ex b/test/support/repo.ex deleted file mode 100644 index 4e6fb658..00000000 --- a/test/support/repo.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Flop.Repo do - use Ecto.Repo, - otp_app: :flop, - adapter: Ecto.Adapters.Postgres -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4897e3bb..869559e7 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1 @@ -{:ok, _pid} = Flop.Repo.start_link() -{:ok, _} = Application.ensure_all_started(:ex_machina) ExUnit.start() From de075fcc76a72ee433f44a34ee5e8871f826f281 Mon Sep 17 00:00:00 2001 From: martosaur Date: Fri, 5 Jul 2024 18:59:51 -0700 Subject: [PATCH 02/18] Make coverall collect coverage from all runs --- .github/workflows/ci.yml | 2 +- mix.exs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b84f5188..87dc05b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: mix local.hex --force mix deps.get - name: Run Tests - run: mix coveralls.json + run: mix coveralls.json.all - uses: codecov/codecov-action@v4 with: files: ./cover/excoveralls.json diff --git a/mix.exs b/mix.exs index 5aeca88d..ae2e1ddf 100644 --- a/mix.exs +++ b/mix.exs @@ -20,6 +20,7 @@ defmodule Flop.MixProject do "coveralls.github": :test, "coveralls.html": :test, "coveralls.json": :test, + "coveralls.json.all": :test, "coveralls.post": :test, "ecto.create": :test, "ecto.drop": :test, @@ -109,7 +110,11 @@ defmodule Flop.MixProject do defp aliases do [ "test.all": ["test", "test.adapters"], - "test.adapters": &test_adapters/1 + "test.adapters": &test_adapters/1, + "coveralls.json.all": [ + "test.adapters --cover", + "coveralls.json --import-cover cover" + ] ] end @@ -124,7 +129,9 @@ defmodule Flop.MixProject do IO.puts("==> Running tests for ECTO_ADAPTER=#{adapter} mix test") {_, res} = - System.cmd("mix", ["test", ansi_option() | args], + System.cmd( + "mix", + ["test", ansi_option(), "--export-coverage=#{adapter}" | args], into: IO.binstream(:stdio, :line), env: [{"ECTO_ADAPTER", adapter}] ) From a3ebf4b8760956b5c70de534c20c76a7af0f0943 Mon Sep 17 00:00:00 2001 From: martosaur Date: Fri, 5 Jul 2024 19:38:16 -0700 Subject: [PATCH 03/18] Add SQLite integration tests --- .gitignore | 3 ++ config/test.exs | 7 ----- integration_test/sqlite/all_test.exs | 1 + integration_test/sqlite/migration.exs | 37 +++++++++++++++++++++++++ integration_test/sqlite/test_helper.exs | 37 +++++++++++++++++++++++++ mix.exs | 3 +- mix.lock | 4 +++ 7 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 integration_test/sqlite/all_test.exs create mode 100644 integration_test/sqlite/migration.exs create mode 100644 integration_test/sqlite/test_helper.exs diff --git a/.gitignore b/.gitignore index c1619158..a8311262 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ flop-*.tar # Ignore dialyzer PLT .plts + +# Test SQLite DB lives here +/tmp/ \ No newline at end of file diff --git a/config/test.exs b/config/test.exs index d9917faf..7941cf14 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,13 +4,6 @@ config :flop, ecto_repos: [Flop.Repo], repo: Flop.Repo -config :flop, Flop.Repo, - username: "postgres", - password: "postgres", - database: "flop_test#{System.get_env("MIX_TEST_PARTITION")}", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox - config :stream_data, max_runs: if(System.get_env("CI"), do: 100, else: 50), max_run_time: if(System.get_env("CI"), do: 3000, else: 200) diff --git a/integration_test/sqlite/all_test.exs b/integration_test/sqlite/all_test.exs new file mode 100644 index 00000000..beb5cfcd --- /dev/null +++ b/integration_test/sqlite/all_test.exs @@ -0,0 +1 @@ +Code.require_file "../cases/flop_test.exs", __DIR__ \ No newline at end of file diff --git a/integration_test/sqlite/migration.exs b/integration_test/sqlite/migration.exs new file mode 100644 index 00000000..5308cca5 --- /dev/null +++ b/integration_test/sqlite/migration.exs @@ -0,0 +1,37 @@ +defmodule Flop.Repo.SQLite.Migration do + use Ecto.Migration + + def change do + create table(:owners) do + add :age, :integer + add :email, :string + add :name, :string + add :tags, {:array, :string} + add :attributes, :map + add :extra, {:map, :string} + end + + create table(:pets) do + add :age, :integer + add :family_name, :string + add :given_name, :string + add :name, :string + add :owner_id, references(:owners) + add :species, :string + add :mood, :string + add :tags, {:array, :string} + end + + create table(:fruits) do + add :family, :string + add :name, :string + add :attributes, :map + add :extra, {:map, :string} + add :owner_id, references(:owners) + end + + create table(:walking_distances) do + add(:trip, :distance) + end + end +end \ No newline at end of file diff --git a/integration_test/sqlite/test_helper.exs b/integration_test/sqlite/test_helper.exs new file mode 100644 index 00000000..7c7cd047 --- /dev/null +++ b/integration_test/sqlite/test_helper.exs @@ -0,0 +1,37 @@ +Application.put_env(:flop, :async_integration_tests, false) + +# Configure SQLite db +Application.put_env(:flop, Flop.Repo, + database: "tmp/test.db", + pool: Ecto.Adapters.SQL.Sandbox, + show_sensitive_data_on_connection_error: true +) + +defmodule Flop.Repo do + use Ecto.Repo, + otp_app: :flop, + adapter: Ecto.Adapters.SQLite3 +end + +defmodule Flop.Integration.Case do + use ExUnit.CaseTemplate + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Flop.Repo) + end +end + +Code.require_file "migration.exs", __DIR__ + +{:ok, _} = Ecto.Adapters.SQLite3.ensure_all_started(Flop.Repo.config(), :temporary) + +# Load up the repository, start it, and run migrations +_ = Ecto.Adapters.SQLite3.storage_down(Flop.Repo.config()) +:ok = Ecto.Adapters.SQLite3.storage_up(Flop.Repo.config()) + +{:ok, _pid} = Flop.Repo.start_link() + +:ok = Ecto.Migrator.up(Flop.Repo, 0, Flop.Repo.SQLite.Migration, log: false) + +{:ok, _} = Application.ensure_all_started(:ex_machina) +ExUnit.start() \ No newline at end of file diff --git a/mix.exs b/mix.exs index ae2e1ddf..ba4b1fd7 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Flop.MixProject do @source_url "https://github.com/woylie/flop" @version "0.25.0" - @adapters ~w(pg) + @adapters ~w(pg sqlite) def project do [ @@ -70,6 +70,7 @@ defmodule Flop.MixProject do {:excoveralls, "~> 0.10", only: :test}, {:nimble_options, "~> 1.0"}, {:postgrex, ">= 0.0.0", only: :test}, + {:ecto_sqlite3, "~> 0.16.0", only: :test}, {:stream_data, "~> 1.0", only: [:dev, :test]} ] end diff --git a/mix.lock b/mix.lock index 0c530f5d..31150655 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, @@ -7,10 +8,13 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"}, + "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, + "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, From 62b1b68bb7fc55cad003d9104d3a01d253b95958 Mon Sep 17 00:00:00 2001 From: martosaur Date: Sun, 4 Aug 2024 15:02:20 -0700 Subject: [PATCH 04/18] add test.postgres and test.sqlite aliases --- mix.exs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index ba4b1fd7..ba6b8782 100644 --- a/mix.exs +++ b/mix.exs @@ -111,6 +111,8 @@ defmodule Flop.MixProject do defp aliases do [ "test.all": ["test", "test.adapters"], + "test.postgres": &test_adapters(["pg"], &1), + "test.sqlite": &test_adapters(["sqlite"], &1), "test.adapters": &test_adapters/1, "coveralls.json.all": [ "test.adapters --cover", @@ -125,8 +127,8 @@ defmodule Flop.MixProject do defp test_paths(nil), do: ["test"] defp test_paths(other), do: raise("unknown adapter #{inspect(other)}") - defp test_adapters(args) do - for adapter <- @adapters do + defp test_adapters(adapters \\ @adapters, args) do + for adapter <- adapters do IO.puts("==> Running tests for ECTO_ADAPTER=#{adapter} mix test") {_, res} = From 142b9dd2a4202da9482ab23c24754c39e123a410 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:07:22 +0900 Subject: [PATCH 05/18] update dependencies --- mix.exs | 2 +- mix.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.exs b/mix.exs index 4dd679c2..b7c9ebf4 100644 --- a/mix.exs +++ b/mix.exs @@ -70,7 +70,7 @@ defmodule Flop.MixProject do {:excoveralls, "~> 0.10", only: :test}, {:nimble_options, "~> 1.0"}, {:postgrex, ">= 0.0.0", only: :test}, - {:ecto_sqlite3, "~> 0.16.0", only: :test}, + {:ecto_sqlite3, "~> 0.17.0", only: :test}, {:stream_data, "~> 1.0", only: [:dev, :test]} ] end diff --git a/mix.lock b/mix.lock index 6f621abe..ef439626 100644 --- a/mix.lock +++ b/mix.lock @@ -2,28 +2,28 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"}, + "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.2", "200226e057f76c40be55fbac77771eb1a233260ab8ec7283f5da6d9402bde8de", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "a3838919c5a34c268c28cafab87b910bcda354a9a4e778658da46c149bb2c1da"}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, - "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, + "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_diff": {:hex, :makeup_diff, "0.1.0", "5be352b6aa6f07fa6a236e3efd7ba689a03f28fb5d35b7a0fa0a1e4a64f6d8bb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "186bad5bb433a8afeb16b01423950e440072284a4103034ca899180343b9b4ac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, + "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, "stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } From f8f070adc07cceff40b4d52d6172303083d6bd1f Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:19:45 +0900 Subject: [PATCH 06/18] remove obsolete version attribute --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 70577558..7f425dfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.7" services: postgres: image: postgres:12-alpine From 56b4b26849c4a1e2f0acfda8579f457f3abc657d Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:28:07 +0900 Subject: [PATCH 07/18] add integration_test folder to formatter config, format --- .formatter.exs | 5 ++- integration_test/cases/flop_test.exs | 50 +++++++++++++------------ integration_test/pg/all_test.exs | 2 +- integration_test/pg/test_helper.exs | 7 ++-- integration_test/sqlite/all_test.exs | 2 +- integration_test/sqlite/migration.exs | 42 ++++++++++----------- integration_test/sqlite/test_helper.exs | 11 +++--- 7 files changed, 63 insertions(+), 56 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 16d5948a..2e761db4 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,9 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + inputs: [ + "{mix,.formatter}.exs", + "{config,integration_test,lib,test}/**/*.{ex,exs}" + ], line_length: 80, import_deps: [:ecto, :stream_data] ] diff --git a/integration_test/cases/flop_test.exs b/integration_test/cases/flop_test.exs index b67b9814..9b776951 100644 --- a/integration_test/cases/flop_test.exs +++ b/integration_test/cases/flop_test.exs @@ -1,14 +1,16 @@ defmodule Flop.Integration.FlopTest do - use Flop.Integration.Case, async: Application.compile_env(:flop, :async_integration_tests, true) + use Flop.Integration.Case, + async: Application.compile_env(:flop, :async_integration_tests, true) + use ExUnitProperties - + doctest Flop, import: true - + import Ecto.Query import Flop.Factory import Flop.Generators import Flop.TestUtil - + alias __MODULE__.TestProvider alias Ecto.Query.QueryExpr alias Flop.Filter @@ -16,19 +18,19 @@ defmodule Flop.Integration.FlopTest do alias Flop.Repo alias MyApp.Owner alias MyApp.Pet - + @pet_count_range 1..200 - + defmodule TestProvider do use Flop, repo: Flop.Repo, default_limit: 35 end - + defmodule TestProviderNested do use Flop, adapter_opts: [repo: Flop.Repo], default_limit: 35 end - + describe "ordering" do test "adds order_by to query if set" do pets = insert_list(20, :pet) @@ -166,7 +168,7 @@ defmodule Flop.Integration.FlopTest do ) == Enum.reverse(expected) end end - + describe "filtering" do property "applies equality filter" do check all pet_count <- integer(@pet_count_range), @@ -256,14 +258,14 @@ defmodule Flop.Integration.FlopTest do checkin_checkout() end end - + # test "applies empty filter" do # require Flop.Adapter.Ecto.Operators - + # field = :species - + # d1 = dynamic([r], is_nil(field(r, ^field)) == ^true); d2 = dynamic([r], Flop.Adapter.Ecto.Operators.empty(:other) == ^true) - + # assert where(Pet, ^d1) == where(Pet, ^d2) # end @@ -728,7 +730,7 @@ defmodule Flop.Integration.FlopTest do assert Flop.query(Pet, flop) == Pet end end - + describe "all/3" do test "returns all matching entries" do matching_pets = insert_list(6, :pet, age: 5) @@ -754,7 +756,7 @@ defmodule Flop.Integration.FlopTest do refute Flop.all(Pet, %Flop{}, query_opts: [prefix: "other_schema"]) == [] end end - + describe "count/3" do test "returns count of matching entries" do _matching_pets = insert_list(6, :pet, age: 5) @@ -838,7 +840,7 @@ defmodule Flop.Integration.FlopTest do assert Flop.count(q, flop) == 3 end end - + describe "meta/3" do test "returns the meta information for a query with limit/offset" do _matching_pets = insert_list(7, :pet, age: 5) @@ -1005,7 +1007,7 @@ defmodule Flop.Integration.FlopTest do assert opts[:for] == Pet end end - + describe "run/3" do test "returns data and meta data" do insert_list(3, :pet) @@ -1013,7 +1015,7 @@ defmodule Flop.Integration.FlopTest do assert {[%Pet{}], %Meta{}} = Flop.run(Pet, flop) end end - + describe "validate_and_run/3" do test "returns error if flop is invalid" do flop = %Flop{ @@ -1037,7 +1039,7 @@ defmodule Flop.Integration.FlopTest do assert {:ok, {[%Pet{}], %Meta{}}} = Flop.validate_and_run(Pet, flop) end end - + describe "validate_and_run!/3" do test "raises if flop is invalid" do assert_raise Flop.InvalidParamsError, fn -> @@ -1051,7 +1053,7 @@ defmodule Flop.Integration.FlopTest do assert {[%Pet{}], %Meta{}} = Flop.validate_and_run!(Pet, flop) end end - + describe "offset-based pagination" do test "applies limit to query" do insert_list(6, :pet) @@ -1096,7 +1098,7 @@ defmodule Flop.Integration.FlopTest do assert Repo.all(query) == Enum.slice(sorted_pets, 8..11) end end - + describe "cursor pagination" do property "querying cursor by cursor forward includes all items in order" do check all pets <- uniq_list_of_pets(length: 1..25), @@ -1719,7 +1721,7 @@ defmodule Flop.Integration.FlopTest do ) end end - + describe "__using__/1" do test "defines wrapper functions that pass default options" do insert_list(3, :pet) @@ -1742,7 +1744,7 @@ defmodule Flop.Integration.FlopTest do assert Keyword.get(opts, :backend) == TestProvider end end - + describe "__using__/1 with nested adapter options" do test "defines wrapper functions that pass default options" do insert_list(3, :pet) @@ -1765,4 +1767,4 @@ defmodule Flop.Integration.FlopTest do assert Keyword.get(opts, :backend) == TestProviderNested end end -end \ No newline at end of file +end diff --git a/integration_test/pg/all_test.exs b/integration_test/pg/all_test.exs index beb5cfcd..215fc306 100644 --- a/integration_test/pg/all_test.exs +++ b/integration_test/pg/all_test.exs @@ -1 +1 @@ -Code.require_file "../cases/flop_test.exs", __DIR__ \ No newline at end of file +Code.require_file("../cases/flop_test.exs", __DIR__) diff --git a/integration_test/pg/test_helper.exs b/integration_test/pg/test_helper.exs index 3f332519..6a0b70ba 100644 --- a/integration_test/pg/test_helper.exs +++ b/integration_test/pg/test_helper.exs @@ -23,10 +23,11 @@ defmodule Flop.Integration.Case do end end -{:ok, _} = Ecto.Adapters.Postgres.ensure_all_started(Flop.Repo.config(), :temporary) +{:ok, _} = + Ecto.Adapters.Postgres.ensure_all_started(Flop.Repo.config(), :temporary) # Load up the repository, start it, and run migrations -_ = Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) +_ = Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) :ok = Ecto.Adapters.Postgres.storage_up(Flop.Repo.config()) {:ok, _pid} = Flop.Repo.start_link() @@ -35,4 +36,4 @@ _ = Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) Ecto.Adapters.SQL.Sandbox.mode(Flop.Repo, :manual) {:ok, _} = Application.ensure_all_started(:ex_machina) -ExUnit.start() \ No newline at end of file +ExUnit.start() diff --git a/integration_test/sqlite/all_test.exs b/integration_test/sqlite/all_test.exs index beb5cfcd..215fc306 100644 --- a/integration_test/sqlite/all_test.exs +++ b/integration_test/sqlite/all_test.exs @@ -1 +1 @@ -Code.require_file "../cases/flop_test.exs", __DIR__ \ No newline at end of file +Code.require_file("../cases/flop_test.exs", __DIR__) diff --git a/integration_test/sqlite/migration.exs b/integration_test/sqlite/migration.exs index 5308cca5..49d469e6 100644 --- a/integration_test/sqlite/migration.exs +++ b/integration_test/sqlite/migration.exs @@ -3,35 +3,35 @@ defmodule Flop.Repo.SQLite.Migration do def change do create table(:owners) do - add :age, :integer - add :email, :string - add :name, :string - add :tags, {:array, :string} - add :attributes, :map - add :extra, {:map, :string} + add(:age, :integer) + add(:email, :string) + add(:name, :string) + add(:tags, {:array, :string}) + add(:attributes, :map) + add(:extra, {:map, :string}) end create table(:pets) do - add :age, :integer - add :family_name, :string - add :given_name, :string - add :name, :string - add :owner_id, references(:owners) - add :species, :string - add :mood, :string - add :tags, {:array, :string} + add(:age, :integer) + add(:family_name, :string) + add(:given_name, :string) + add(:name, :string) + add(:owner_id, references(:owners)) + add(:species, :string) + add(:mood, :string) + add(:tags, {:array, :string}) end create table(:fruits) do - add :family, :string - add :name, :string - add :attributes, :map - add :extra, {:map, :string} - add :owner_id, references(:owners) + add(:family, :string) + add(:name, :string) + add(:attributes, :map) + add(:extra, {:map, :string}) + add(:owner_id, references(:owners)) end - + create table(:walking_distances) do add(:trip, :distance) end end -end \ No newline at end of file +end diff --git a/integration_test/sqlite/test_helper.exs b/integration_test/sqlite/test_helper.exs index 7c7cd047..37102cea 100644 --- a/integration_test/sqlite/test_helper.exs +++ b/integration_test/sqlite/test_helper.exs @@ -15,18 +15,19 @@ end defmodule Flop.Integration.Case do use ExUnit.CaseTemplate - + setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Flop.Repo) end end -Code.require_file "migration.exs", __DIR__ +Code.require_file("migration.exs", __DIR__) -{:ok, _} = Ecto.Adapters.SQLite3.ensure_all_started(Flop.Repo.config(), :temporary) +{:ok, _} = + Ecto.Adapters.SQLite3.ensure_all_started(Flop.Repo.config(), :temporary) # Load up the repository, start it, and run migrations -_ = Ecto.Adapters.SQLite3.storage_down(Flop.Repo.config()) +_ = Ecto.Adapters.SQLite3.storage_down(Flop.Repo.config()) :ok = Ecto.Adapters.SQLite3.storage_up(Flop.Repo.config()) {:ok, _pid} = Flop.Repo.start_link() @@ -34,4 +35,4 @@ _ = Ecto.Adapters.SQLite3.storage_down(Flop.Repo.config()) :ok = Ecto.Migrator.up(Flop.Repo, 0, Flop.Repo.SQLite.Migration, log: false) {:ok, _} = Application.ensure_all_started(:ex_machina) -ExUnit.start() \ No newline at end of file +ExUnit.start() From 1ae36b5630ee7cf95d526bfb72f038aa1aaaa564 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:59:03 +0900 Subject: [PATCH 08/18] collocate postgres migrations --- integration_test/pg/migration.exs | 57 +++++++++++++++++++ integration_test/pg/test_helper.exs | 9 ++- integration_test/sqlite/migration.exs | 2 +- priv/repo/migrations/.formatter.exs | 5 -- .../20200527145236_create_test_tables.exs | 33 ----------- ...523123456_create_pets_in_second_schema.exs | 18 ------ .../20230614113912_composite_type.exs | 11 ---- .../20230614114123_create_distance_table.exs | 9 --- 8 files changed, 64 insertions(+), 80 deletions(-) create mode 100644 integration_test/pg/migration.exs delete mode 100644 priv/repo/migrations/.formatter.exs delete mode 100644 priv/repo/migrations/20200527145236_create_test_tables.exs delete mode 100644 priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs delete mode 100644 priv/repo/migrations/20230614113912_composite_type.exs delete mode 100644 priv/repo/migrations/20230614114123_create_distance_table.exs diff --git a/integration_test/pg/migration.exs b/integration_test/pg/migration.exs new file mode 100644 index 00000000..7a9a25fc --- /dev/null +++ b/integration_test/pg/migration.exs @@ -0,0 +1,57 @@ +defmodule Flop.Repo.Postgres.Migration do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE public.distance AS (unit varchar, value float);", + "DROP TYPE public.distance;" + ) + + create table(:owners) do + add(:age, :integer) + add(:email, :string) + add(:name, :string) + add(:tags, {:array, :string}) + add(:attributes, :map) + add(:extra, {:map, :string}) + end + + create table(:pets) do + add(:age, :integer) + add(:family_name, :string) + add(:given_name, :string) + add(:name, :string) + add(:owner_id, references(:owners)) + add(:species, :string) + add(:mood, :string) + add(:tags, {:array, :string}) + end + + create table(:fruits) do + add(:family, :string) + add(:name, :string) + add(:attributes, :map) + add(:extra, {:map, :string}) + add(:owner_id, references(:owners)) + end + + create table(:walking_distances) do + add(:trip, :distance) + end + + # create pets table in other schema + + execute("CREATE SCHEMA other_schema;", "DROP SCHEMA other_schema;") + + create table(:pets, prefix: "other_schema") do + add(:age, :integer) + add(:family_name, :string) + add(:given_name, :string) + add(:name, :string) + add(:owner_id, :integer) + add(:species, :string) + add(:mood, :string) + add(:tags, {:array, :string}) + end + end +end diff --git a/integration_test/pg/test_helper.exs b/integration_test/pg/test_helper.exs index 6a0b70ba..4976711a 100644 --- a/integration_test/pg/test_helper.exs +++ b/integration_test/pg/test_helper.exs @@ -23,16 +23,19 @@ defmodule Flop.Integration.Case do end end +Code.require_file("migration.exs", __DIR__) + {:ok, _} = Ecto.Adapters.Postgres.ensure_all_started(Flop.Repo.config(), :temporary) # Load up the repository, start it, and run migrations -_ = Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) -:ok = Ecto.Adapters.Postgres.storage_up(Flop.Repo.config()) +Ecto.Adapters.Postgres.storage_down(Flop.Repo.config()) +Ecto.Adapters.Postgres.storage_up(Flop.Repo.config()) {:ok, _pid} = Flop.Repo.start_link() -[_ | _] = Ecto.Migrator.run(Flop.Repo, :up, log: true, all: true) +Ecto.Migrator.up(Flop.Repo, 0, Flop.Repo.Postgres.Migration, log: true) + Ecto.Adapters.SQL.Sandbox.mode(Flop.Repo, :manual) {:ok, _} = Application.ensure_all_started(:ex_machina) diff --git a/integration_test/sqlite/migration.exs b/integration_test/sqlite/migration.exs index 49d469e6..ac5eb202 100644 --- a/integration_test/sqlite/migration.exs +++ b/integration_test/sqlite/migration.exs @@ -1,4 +1,4 @@ -defmodule Flop.Repo.SQLite.Migration do +defmodule Flop.Repo.Postgres.Migration do use Ecto.Migration def change do diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs deleted file mode 100644 index 3a510b28..00000000 --- a/priv/repo/migrations/.formatter.exs +++ /dev/null @@ -1,5 +0,0 @@ -[ - import_deps: [:ecto_sql], - inputs: ["*.exs"], - line_length: 80 -] diff --git a/priv/repo/migrations/20200527145236_create_test_tables.exs b/priv/repo/migrations/20200527145236_create_test_tables.exs deleted file mode 100644 index 9c4940e8..00000000 --- a/priv/repo/migrations/20200527145236_create_test_tables.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Flop.Repo.Migrations.CreateTestTables do - use Ecto.Migration - - def change do - create table(:owners) do - add :age, :integer - add :email, :string - add :name, :string - add :tags, {:array, :string} - add :attributes, :map - add :extra, {:map, :string} - end - - create table(:pets) do - add :age, :integer - add :family_name, :string - add :given_name, :string - add :name, :string - add :owner_id, references(:owners) - add :species, :string - add :mood, :string - add :tags, {:array, :string} - end - - create table(:fruits) do - add :family, :string - add :name, :string - add :attributes, :map - add :extra, {:map, :string} - add :owner_id, references(:owners) - end - end -end diff --git a/priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs b/priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs deleted file mode 100644 index d5dee232..00000000 --- a/priv/repo/migrations/20210523123456_create_pets_in_second_schema.exs +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Flop.Repo.Migrations.CreatePetsInSecondSchema do - use Ecto.Migration - - def change do - execute("CREATE SCHEMA other_schema;", "DROP SCHEMA other_schema;") - - create table(:pets, prefix: "other_schema") do - add :age, :integer - add :family_name, :string - add :given_name, :string - add :name, :string - add :owner_id, :integer - add :species, :string - add :mood, :string - add :tags, {:array, :string} - end - end -end diff --git a/priv/repo/migrations/20230614113912_composite_type.exs b/priv/repo/migrations/20230614113912_composite_type.exs deleted file mode 100644 index 564110a3..00000000 --- a/priv/repo/migrations/20230614113912_composite_type.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Sibill.Repo.Migrations.CompositeType do - use Ecto.Migration - - def up do - execute("CREATE TYPE public.distance AS (unit varchar, value float);") - end - - def down do - execute("DROP TYPE public.distance;") - end -end diff --git a/priv/repo/migrations/20230614114123_create_distance_table.exs b/priv/repo/migrations/20230614114123_create_distance_table.exs deleted file mode 100644 index 25296fe9..00000000 --- a/priv/repo/migrations/20230614114123_create_distance_table.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Sibill.Repo.Migrations.CreateDistanceTable do - use Ecto.Migration - - def change do - create table(:walking_distances) do - add(:trip, :distance) - end - end -end From b626239a8ceec4f120e68bf645ed6b5dfc3e1c29 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:01:06 +0900 Subject: [PATCH 09/18] remove keep file --- priv/plts/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 priv/plts/.keep diff --git a/priv/plts/.keep b/priv/plts/.keep deleted file mode 100644 index e69de29b..00000000 From 81590d386a6272d70ebcdec85ce480a2125d003f Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:04:20 +0900 Subject: [PATCH 10/18] move custom type tests --- test/{ => flop}/custom_types/any_test.exs | 0 test/{ => flop}/custom_types/existing_atom_test.exs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => flop}/custom_types/any_test.exs (100%) rename test/{ => flop}/custom_types/existing_atom_test.exs (100%) diff --git a/test/custom_types/any_test.exs b/test/flop/custom_types/any_test.exs similarity index 100% rename from test/custom_types/any_test.exs rename to test/flop/custom_types/any_test.exs diff --git a/test/custom_types/existing_atom_test.exs b/test/flop/custom_types/existing_atom_test.exs similarity index 100% rename from test/custom_types/existing_atom_test.exs rename to test/flop/custom_types/existing_atom_test.exs From 71eda10f8a71f09397810d6a31f2ea67eb5606ea Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:07:38 +0900 Subject: [PATCH 11/18] move base tests to sub folder --- mix.exs | 2 +- test/{ => base}/flop/cursor_test.exs | 0 test/{ => base}/flop/custom_types/any_test.exs | 0 test/{ => base}/flop/custom_types/existing_atom_test.exs | 0 test/{ => base}/flop/filter_test.exs | 0 test/{ => base}/flop/meta_test.exs | 0 test/{ => base}/flop/misc_test.exs | 0 test/{ => base}/flop/relay_test.exs | 0 test/{ => base}/flop/schema_test.exs | 0 test/{ => base}/flop/validation_test.exs | 0 test/{ => base}/flop_test.exs | 0 test/{ => base}/test_helper.exs | 0 12 files changed, 1 insertion(+), 1 deletion(-) rename test/{ => base}/flop/cursor_test.exs (100%) rename test/{ => base}/flop/custom_types/any_test.exs (100%) rename test/{ => base}/flop/custom_types/existing_atom_test.exs (100%) rename test/{ => base}/flop/filter_test.exs (100%) rename test/{ => base}/flop/meta_test.exs (100%) rename test/{ => base}/flop/misc_test.exs (100%) rename test/{ => base}/flop/relay_test.exs (100%) rename test/{ => base}/flop/schema_test.exs (100%) rename test/{ => base}/flop/validation_test.exs (100%) rename test/{ => base}/flop_test.exs (100%) rename test/{ => base}/test_helper.exs (100%) diff --git a/mix.exs b/mix.exs index b7c9ebf4..03ca5fbf 100644 --- a/mix.exs +++ b/mix.exs @@ -124,7 +124,7 @@ defmodule Flop.MixProject do defp test_paths(adapter) when adapter in @adapters, do: ["integration_test/#{adapter}"] - defp test_paths(nil), do: ["test"] + defp test_paths(nil), do: ["test/base"] defp test_paths(other), do: raise("unknown adapter #{inspect(other)}") defp test_adapters(adapters \\ @adapters, args) do diff --git a/test/flop/cursor_test.exs b/test/base/flop/cursor_test.exs similarity index 100% rename from test/flop/cursor_test.exs rename to test/base/flop/cursor_test.exs diff --git a/test/flop/custom_types/any_test.exs b/test/base/flop/custom_types/any_test.exs similarity index 100% rename from test/flop/custom_types/any_test.exs rename to test/base/flop/custom_types/any_test.exs diff --git a/test/flop/custom_types/existing_atom_test.exs b/test/base/flop/custom_types/existing_atom_test.exs similarity index 100% rename from test/flop/custom_types/existing_atom_test.exs rename to test/base/flop/custom_types/existing_atom_test.exs diff --git a/test/flop/filter_test.exs b/test/base/flop/filter_test.exs similarity index 100% rename from test/flop/filter_test.exs rename to test/base/flop/filter_test.exs diff --git a/test/flop/meta_test.exs b/test/base/flop/meta_test.exs similarity index 100% rename from test/flop/meta_test.exs rename to test/base/flop/meta_test.exs diff --git a/test/flop/misc_test.exs b/test/base/flop/misc_test.exs similarity index 100% rename from test/flop/misc_test.exs rename to test/base/flop/misc_test.exs diff --git a/test/flop/relay_test.exs b/test/base/flop/relay_test.exs similarity index 100% rename from test/flop/relay_test.exs rename to test/base/flop/relay_test.exs diff --git a/test/flop/schema_test.exs b/test/base/flop/schema_test.exs similarity index 100% rename from test/flop/schema_test.exs rename to test/base/flop/schema_test.exs diff --git a/test/flop/validation_test.exs b/test/base/flop/validation_test.exs similarity index 100% rename from test/flop/validation_test.exs rename to test/base/flop/validation_test.exs diff --git a/test/flop_test.exs b/test/base/flop_test.exs similarity index 100% rename from test/flop_test.exs rename to test/base/flop_test.exs diff --git a/test/test_helper.exs b/test/base/test_helper.exs similarity index 100% rename from test/test_helper.exs rename to test/base/test_helper.exs From a31558b3acd1c12098ef85b8807bd15752afeb0b Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:12:14 +0900 Subject: [PATCH 12/18] update adapter test folder structure --- .formatter.exs | 2 +- mix.exs | 6 +++--- .../adapters/ecto}/cases/flop_test.exs | 0 .../pg => test/adapters/ecto/postgres}/all_test.exs | 0 .../pg => test/adapters/ecto/postgres}/migration.exs | 0 .../pg => test/adapters/ecto/postgres}/test_helper.exs | 0 .../adapters/ecto}/sqlite/all_test.exs | 0 .../adapters/ecto}/sqlite/migration.exs | 2 +- .../adapters/ecto}/sqlite/test_helper.exs | 0 9 files changed, 5 insertions(+), 5 deletions(-) rename {integration_test => test/adapters/ecto}/cases/flop_test.exs (100%) rename {integration_test/pg => test/adapters/ecto/postgres}/all_test.exs (100%) rename {integration_test/pg => test/adapters/ecto/postgres}/migration.exs (100%) rename {integration_test/pg => test/adapters/ecto/postgres}/test_helper.exs (100%) rename {integration_test => test/adapters/ecto}/sqlite/all_test.exs (100%) rename {integration_test => test/adapters/ecto}/sqlite/migration.exs (95%) rename {integration_test => test/adapters/ecto}/sqlite/test_helper.exs (100%) diff --git a/.formatter.exs b/.formatter.exs index 2e761db4..d7230de2 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,7 +2,7 @@ [ inputs: [ "{mix,.formatter}.exs", - "{config,integration_test,lib,test}/**/*.{ex,exs}" + "{config,lib,test}/**/*.{ex,exs}" ], line_length: 80, import_deps: [:ecto, :stream_data] diff --git a/mix.exs b/mix.exs index 03ca5fbf..fe908067 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Flop.MixProject do @source_url "https://github.com/woylie/flop" @version "0.26.1" - @adapters ~w(pg sqlite) + @adapters ~w(postgres sqlite) def project do [ @@ -111,7 +111,7 @@ defmodule Flop.MixProject do defp aliases do [ "test.all": ["test", "test.adapters"], - "test.postgres": &test_adapters(["pg"], &1), + "test.postgres": &test_adapters(["postgres"], &1), "test.sqlite": &test_adapters(["sqlite"], &1), "test.adapters": &test_adapters/1, "coveralls.json.all": [ @@ -122,7 +122,7 @@ defmodule Flop.MixProject do end defp test_paths(adapter) when adapter in @adapters, - do: ["integration_test/#{adapter}"] + do: ["test/adapters/ecto/#{adapter}"] defp test_paths(nil), do: ["test/base"] defp test_paths(other), do: raise("unknown adapter #{inspect(other)}") diff --git a/integration_test/cases/flop_test.exs b/test/adapters/ecto/cases/flop_test.exs similarity index 100% rename from integration_test/cases/flop_test.exs rename to test/adapters/ecto/cases/flop_test.exs diff --git a/integration_test/pg/all_test.exs b/test/adapters/ecto/postgres/all_test.exs similarity index 100% rename from integration_test/pg/all_test.exs rename to test/adapters/ecto/postgres/all_test.exs diff --git a/integration_test/pg/migration.exs b/test/adapters/ecto/postgres/migration.exs similarity index 100% rename from integration_test/pg/migration.exs rename to test/adapters/ecto/postgres/migration.exs diff --git a/integration_test/pg/test_helper.exs b/test/adapters/ecto/postgres/test_helper.exs similarity index 100% rename from integration_test/pg/test_helper.exs rename to test/adapters/ecto/postgres/test_helper.exs diff --git a/integration_test/sqlite/all_test.exs b/test/adapters/ecto/sqlite/all_test.exs similarity index 100% rename from integration_test/sqlite/all_test.exs rename to test/adapters/ecto/sqlite/all_test.exs diff --git a/integration_test/sqlite/migration.exs b/test/adapters/ecto/sqlite/migration.exs similarity index 95% rename from integration_test/sqlite/migration.exs rename to test/adapters/ecto/sqlite/migration.exs index ac5eb202..49d469e6 100644 --- a/integration_test/sqlite/migration.exs +++ b/test/adapters/ecto/sqlite/migration.exs @@ -1,4 +1,4 @@ -defmodule Flop.Repo.Postgres.Migration do +defmodule Flop.Repo.SQLite.Migration do use Ecto.Migration def change do diff --git a/integration_test/sqlite/test_helper.exs b/test/adapters/ecto/sqlite/test_helper.exs similarity index 100% rename from integration_test/sqlite/test_helper.exs rename to test/adapters/ecto/sqlite/test_helper.exs From 113b025c6c650836b21d727a5b738f50ce0d3099 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:29:05 +0900 Subject: [PATCH 13/18] rename module --- test/adapters/ecto/cases/flop_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapters/ecto/cases/flop_test.exs b/test/adapters/ecto/cases/flop_test.exs index 9b776951..5e8d99f5 100644 --- a/test/adapters/ecto/cases/flop_test.exs +++ b/test/adapters/ecto/cases/flop_test.exs @@ -1,4 +1,4 @@ -defmodule Flop.Integration.FlopTest do +defmodule Flop.Adapters.Ecto.FlopTest do use Flop.Integration.Case, async: Application.compile_env(:flop, :async_integration_tests, true) From ba2439b14eed4f450386c8770d2ba8bb75897a33 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:37:11 +0900 Subject: [PATCH 14/18] improve error message --- mix.exs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index fe908067..50f6dc46 100644 --- a/mix.exs +++ b/mix.exs @@ -125,7 +125,16 @@ defmodule Flop.MixProject do do: ["test/adapters/ecto/#{adapter}"] defp test_paths(nil), do: ["test/base"] - defp test_paths(other), do: raise("unknown adapter #{inspect(other)}") + + defp test_paths(adapter) do + raise """ + unknown Ecto adapter + + Expected ECTO_ADAPTER to be one of: #{inspect(@adapters)} + + Got: #{inspect(adapter)} + """ + end defp test_adapters(adapters \\ @adapters, args) do for adapter <- adapters do From da7e72a99bededd9076f5692bd6e3393bea3a2e0 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:47:57 +0900 Subject: [PATCH 15/18] skip schema prefix tests for sqlite --- test/adapters/ecto/cases/flop_test.exs | 3 +++ test/adapters/ecto/sqlite/test_helper.exs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/adapters/ecto/cases/flop_test.exs b/test/adapters/ecto/cases/flop_test.exs index 5e8d99f5..97154d24 100644 --- a/test/adapters/ecto/cases/flop_test.exs +++ b/test/adapters/ecto/cases/flop_test.exs @@ -749,6 +749,7 @@ defmodule Flop.Adapters.Ecto.FlopTest do assert Enum.map(Flop.all(Pet, flop), & &1.name) == [name_1, name_2] end + @tag :prefix test "can apply a query prefix" do insert(:pet, %{}, prefix: "other_schema") @@ -772,6 +773,7 @@ defmodule Flop.Adapters.Ecto.FlopTest do assert Flop.count(Pet, flop) == 6 end + @tag :prefix test "can apply a query prefix" do insert(:pet, %{}, prefix: "other_schema") @@ -985,6 +987,7 @@ defmodule Flop.Adapters.Ecto.FlopTest do Flop.meta(Pet, %Flop{page_size: 3, page: 2}) end + @tag :prefix test "can apply a query prefix" do insert(:pet, %{}, prefix: "other_schema") diff --git a/test/adapters/ecto/sqlite/test_helper.exs b/test/adapters/ecto/sqlite/test_helper.exs index 37102cea..8a34bdb8 100644 --- a/test/adapters/ecto/sqlite/test_helper.exs +++ b/test/adapters/ecto/sqlite/test_helper.exs @@ -35,4 +35,4 @@ _ = Ecto.Adapters.SQLite3.storage_down(Flop.Repo.config()) :ok = Ecto.Migrator.up(Flop.Repo, 0, Flop.Repo.SQLite.Migration, log: false) {:ok, _} = Application.ensure_all_started(:ex_machina) -ExUnit.start() +ExUnit.start(exclude: [:prefix]) From 2b1b8c8909b6e81540fc1d14c8596f8281152427 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:45:11 +0900 Subject: [PATCH 16/18] add mix coveralls.html.all --- mix.exs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mix.exs b/mix.exs index 50f6dc46..8bfb474c 100644 --- a/mix.exs +++ b/mix.exs @@ -19,6 +19,7 @@ defmodule Flop.MixProject do "coveralls.detail": :test, "coveralls.github": :test, "coveralls.html": :test, + "coveralls.html.all": :test, "coveralls.json": :test, "coveralls.json.all": :test, "coveralls.post": :test, @@ -114,6 +115,10 @@ defmodule Flop.MixProject do "test.postgres": &test_adapters(["postgres"], &1), "test.sqlite": &test_adapters(["sqlite"], &1), "test.adapters": &test_adapters/1, + "coveralls.html.all": [ + "test.adapters --cover", + "coveralls.html --import-cover cover" + ], "coveralls.json.all": [ "test.adapters --cover", "coveralls.json --import-cover cover" From cf84359e5cd074007a14a48f0f39ac8b40e8a607 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:02:42 +0900 Subject: [PATCH 17/18] only run base/postgres tests on CI for now --- .github/workflows/ci.yml | 6 ++++-- mix.exs | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76719c8f..3230f1b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,8 +112,10 @@ jobs: mix local.rebar --force mix local.hex --force mix deps.get - - name: Run Tests - run: mix test.all + - name: Run Base Tests + run: mix test + - name: Run Postgres Tests + run: mix test.postgres matrix-results: if: ${{ always() }} diff --git a/mix.exs b/mix.exs index 8bfb474c..5e04e4ec 100644 --- a/mix.exs +++ b/mix.exs @@ -120,7 +120,9 @@ defmodule Flop.MixProject do "coveralls.html --import-cover cover" ], "coveralls.json.all": [ - "test.adapters --cover", + # only run postgres and base tests for coverage until sqlite tests are + # fixed + fn _ -> test_adapters(["postgres"], ["--cover"]) end, "coveralls.json --import-cover cover" ] ] From c2aed56538f9822efc6a14f76d9aafe593161996 Mon Sep 17 00:00:00 2001 From: woylie <13847569+woylie@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:04:32 +0900 Subject: [PATCH 18/18] satisfy credo --- test/adapters/ecto/postgres/test_helper.exs | 3 ++- test/adapters/ecto/sqlite/test_helper.exs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/adapters/ecto/postgres/test_helper.exs b/test/adapters/ecto/postgres/test_helper.exs index 4976711a..d0d4f5e3 100644 --- a/test/adapters/ecto/postgres/test_helper.exs +++ b/test/adapters/ecto/postgres/test_helper.exs @@ -17,9 +17,10 @@ end defmodule Flop.Integration.Case do use ExUnit.CaseTemplate + alias Ecto.Adapters.SQL.Sandbox setup do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Flop.Repo) + :ok = Sandbox.checkout(Flop.Repo) end end diff --git a/test/adapters/ecto/sqlite/test_helper.exs b/test/adapters/ecto/sqlite/test_helper.exs index 8a34bdb8..b17ca214 100644 --- a/test/adapters/ecto/sqlite/test_helper.exs +++ b/test/adapters/ecto/sqlite/test_helper.exs @@ -15,9 +15,10 @@ end defmodule Flop.Integration.Case do use ExUnit.CaseTemplate + alias Ecto.Adapters.SQL.Sandbox setup do - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Flop.Repo) + :ok = Sandbox.checkout(Flop.Repo) end end