From f378e0774030ababa0af609579b3790378cc09d2 Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Tue, 31 Dec 2019 11:25:01 -0800 Subject: [PATCH 1/3] WIP --- lib/arbor/adapters/postgres.ex | 22 ++++++++++++++++++++++ mix.exs | 4 ++++ mix.lock | 3 +++ test/arbor/adapters/postgres_test.exs | 16 ++++++++++++++++ test/test_helper.exs | 4 +++- 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 lib/arbor/adapters/postgres.ex create mode 100644 test/arbor/adapters/postgres_test.exs diff --git a/lib/arbor/adapters/postgres.ex b/lib/arbor/adapters/postgres.ex new file mode 100644 index 0000000..303791d --- /dev/null +++ b/lib/arbor/adapters/postgres.ex @@ -0,0 +1,22 @@ +defmodule Arbor.Adapters.Postgres do + @moduledoc """ + Postgres tree adapter + """ + + import Ecto.Query + + @doc """ + TODO: Support strings and structs + TODO: Ensure composability + """ + @spec roots(atom(), Keyword.t()) :: Ecto.Query.t() + def roots(schema, opts \\ []) do + # foreign_key = opts |> build_opts |> Keyword.get(:foreign_key) + foreign_key = :parent_id + from(t in schema, where: is_nil(field(t, ^foreign_key))) + end + + defp build_opts(opts) do + opts + end +end diff --git a/mix.exs b/mix.exs index 78cb665..03dfc68 100644 --- a/mix.exs +++ b/mix.exs @@ -31,6 +31,10 @@ defmodule Arbor.Mixfile do [ {:ecto_sql, ">= 3.0.0"}, {:postgrex, ">= 0.0.0"}, + + ## Test / Dev + {:mix_test_watch, "~> 1.0", only: :dev, runtime: false}, + {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, {:earmark, "~> 1.1", only: [:docs, :dev]}, {:ex_doc, "~> 0.19", only: [:docs, :dev]} ] diff --git a/mix.lock b/mix.lock index f6a3a1c..f4b351b 100644 --- a/mix.lock +++ b/mix.lock @@ -2,12 +2,15 @@ "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.21.1", "5ac36660846967cd869255f4426467a11672fec3d8db602c429425ce5b613b90", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.15.0", "dd5349161019caeea93efa42f9b22f9d79995c3a86bdffb796427b4c9863b0f0", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, diff --git a/test/arbor/adapters/postgres_test.exs b/test/arbor/adapters/postgres_test.exs new file mode 100644 index 0000000..535abc7 --- /dev/null +++ b/test/arbor/adapters/postgres_test.exs @@ -0,0 +1,16 @@ +defmodule Arbor.Adapters.PostgresTest do + use Arbor.TestCase, async: true + alias Arbor.Adapters.Postgres, as: PGAdapter + + describe "roots/2" do + test "given an integer primary key, returns a root node query" do + [dog_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("dogs") + [cat_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("cats") + + query = PGAdapter.roots(Comment) + roots = Repo.all(query) + + assert roots == [dog_root, cat_root] + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index fcd0803..3c70b87 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -19,7 +19,9 @@ defmodule Arbor.TestCase do folder |> Repo.insert!() end - def create_chatter(subject) do + def create_chatter(subject), do: create_conversation(subject) + + def create_conversation(subject) do root = %Comment{body: "Lets talk about #{subject}"} |> Repo.insert!() branch1 = From 3ca4341638ffcbcab14df7449b7448b04c213131 Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Tue, 31 Dec 2019 12:20:57 -0800 Subject: [PATCH 2/3] roots/N migrated --- lib/arbor/adapters/postgres.ex | 28 ++++++++---- lib/arbor/tree.ex | 56 ++++++++++++----------- test/arbor/adapters/postgres_test.exs | 64 ++++++++++++++++++++++++++- test/support/comment.ex | 7 +++ 4 files changed, 119 insertions(+), 36 deletions(-) diff --git a/lib/arbor/adapters/postgres.ex b/lib/arbor/adapters/postgres.ex index 303791d..f3e1790 100644 --- a/lib/arbor/adapters/postgres.ex +++ b/lib/arbor/adapters/postgres.ex @@ -6,17 +6,27 @@ defmodule Arbor.Adapters.Postgres do import Ecto.Query @doc """ - TODO: Support strings and structs - TODO: Ensure composability + ## Examples + Basic Usage: + iex> Arbor.Adapters.Postgres.roots(Arbor.Comment) + #Ecto.Query + + Providing parent foreign key name: + iex> Arbor.Adapters.Postgres.roots(Arbor.Folder, foreign_key: :parent_uuid) + #Ecto.Query + + Ad-hoc queries: + iex> Arbor.Adapters.Postgres.roots("comments") + #Ecto.Query + + Composing queries: + iex> roots = Arbor.Adapters.Postgres.roots(Arbor.Comment) + ...> sorted_roots = Arbor.Comment.by_inserted_at(roots) + #Ecto.Query """ - @spec roots(atom(), Keyword.t()) :: Ecto.Query.t() + @spec roots(module() | String.t(), Keyword.t()) :: Ecto.Query.t() def roots(schema, opts \\ []) do - # foreign_key = opts |> build_opts |> Keyword.get(:foreign_key) - foreign_key = :parent_id + foreign_key = Keyword.get(opts, :foreign_key, :parent_id) from(t in schema, where: is_nil(field(t, ^foreign_key))) end - - defp build_opts(opts) do - opts - end end diff --git a/lib/arbor/tree.ex b/lib/arbor/tree.ex index db31dcf..dad04b2 100644 --- a/lib/arbor/tree.ex +++ b/lib/arbor/tree.ex @@ -74,10 +74,9 @@ defmodule Arbor.Tree do import Ecto.Query def roots do - from( - t in unquote(definition), - where: fragment(unquote("#{opts[:foreign_key]} IS NULL")) - ) + schema = unquote(definition) + foreign_key = unquote(opts[:foreign_key]) + Arbor.Adapters.Postgres.roots(schema, foreign_key: foreign_key) end def parent(struct) do @@ -117,28 +116,35 @@ defmodule Arbor.Tree do end def ancestors(struct) do - from t in unquote(definition), - join: g in fragment(unquote(""" - ( - WITH RECURSIVE #{opts[:tree_name]} AS ( - SELECT #{opts[:primary_key]}, - #{opts[:foreign_key]}, - 0 AS depth - FROM #{opts[:table_name]} - WHERE #{opts[:primary_key]} = ? - UNION ALL - SELECT #{opts[:table_name]}.#{opts[:primary_key]}, - #{opts[:table_name]}.#{opts[:foreign_key]}, - #{opts[:tree_name]}.depth + 1 - FROM #{opts[:table_name]} - JOIN #{opts[:tree_name]} - ON #{opts[:tree_name]}.#{opts[:foreign_key]} = #{opts[:table_name]}.#{opts[:primary_key]} - ) - SELECT * - FROM #{opts[:tree_name]} - ) - """), type(^struct.unquote(opts[:primary_key]), unquote(opts[:primary_key_type]))), + from(t in unquote(definition), + join: + g in fragment( + unquote(""" + ( + WITH RECURSIVE #{opts[:tree_name]} AS ( + SELECT #{opts[:primary_key]}, + #{opts[:foreign_key]}, + 0 AS depth + FROM #{opts[:table_name]} + WHERE #{opts[:primary_key]} = ? + UNION ALL + SELECT #{opts[:table_name]}.#{opts[:primary_key]}, + #{opts[:table_name]}.#{opts[:foreign_key]}, + #{opts[:tree_name]}.depth + 1 + FROM #{opts[:table_name]} + JOIN #{opts[:tree_name]} + ON #{opts[:tree_name]}.#{opts[:foreign_key]} = #{opts[:table_name]}.#{ + opts[:primary_key] + } + ) + SELECT * + FROM #{opts[:tree_name]} + ) + """), + type(^struct.unquote(opts[:primary_key]), unquote(opts[:primary_key_type])) + ), on: t.unquote(opts[:primary_key]) == g.unquote(opts[:foreign_key]) + ) end def descendants(struct, depth \\ 2_147_483_647) do diff --git a/test/arbor/adapters/postgres_test.exs b/test/arbor/adapters/postgres_test.exs index 535abc7..8839ffa 100644 --- a/test/arbor/adapters/postgres_test.exs +++ b/test/arbor/adapters/postgres_test.exs @@ -2,15 +2,75 @@ defmodule Arbor.Adapters.PostgresTest do use Arbor.TestCase, async: true alias Arbor.Adapters.Postgres, as: PGAdapter - describe "roots/2" do + describe "roots/1" do + test "given a string schema name, returns a root node query" do + [_dog_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("dogs") + [_cat_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("cats") + + query = + "comments" + |> PGAdapter.roots() + |> select([c], {c.body}) + + roots = Repo.all(query) + sorted_roots = Enum.sort_by(roots, fn {body} -> body end) + + assert sorted_roots == [{"Lets talk about cats"}, {"Lets talk about dogs"}] + end + test "given an integer primary key, returns a root node query" do [dog_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("dogs") [cat_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("cats") query = PGAdapter.roots(Comment) roots = Repo.all(query) + sorted_roots = Enum.sort_by(roots, fn root -> root.id end) + + assert sorted_roots == [dog_root, cat_root] + end + + test "queries are composable" do + [dog_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("dogs") + [cat_root, _branch1, _leaf1, _leaf2, _branch2, _leaf3] = create_conversation("cats") + + query = PGAdapter.roots(Comment) + sorted_roots = query |> Arbor.Comment.by_id() |> Repo.all() + + assert sorted_roots == [dog_root, cat_root] + end + + test "given an UUID primary key, returns a root node query" do + chauncys_home = create_folder("chauncy") + create_folder("Documents", parent: chauncys_home) + create_folder("Downloads", parent: chauncys_home) + + rauls_home = create_folder("raul") + create_folder("Documents", parent: rauls_home) + create_folder("Downloads", parent: rauls_home) + + query = PGAdapter.roots(Folder) + roots = Repo.all(query) + sorted_roots = Enum.sort_by(roots, fn root -> root.name end) + + assert sorted_roots == [chauncys_home, rauls_home] + end + end + + describe "roots/2" do + test "given an alternate parent foreign key name, returns a root node query" do + chauncys_home = create_foreign("chauncy") + create_foreign("Documents", parent: chauncys_home) + create_foreign("Downloads", parent: chauncys_home) + + rauls_home = create_foreign("raul") + create_foreign("Documents", parent: rauls_home) + create_foreign("Downloads", parent: rauls_home) + + query = PGAdapter.roots(Foreign, foreign_key: :parent_uuid) + roots = Repo.all(query) + sorted_roots = Enum.sort_by(roots, fn root -> root.name end) - assert roots == [dog_root, cat_root] + assert sorted_roots == [chauncys_home, rauls_home] end end end diff --git a/test/support/comment.ex b/test/support/comment.ex index 1b94603..d405e4f 100644 --- a/test/support/comment.ex +++ b/test/support/comment.ex @@ -15,6 +15,13 @@ defmodule Arbor.Comment do timestamps() end + def by_id(query \\ __MODULE__) do + from( + c in query, + order_by: [asc: :id] + ) + end + def by_inserted_at(query \\ __MODULE__) do from( c in query, From a2fac90756574e513da596e98a28e1f7261b258e Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Tue, 31 Dec 2019 13:06:37 -0800 Subject: [PATCH 3/3] parent/N migrated --- .credo.exs | 153 ++++++++++++++++++++++++++ lib/arbor/adapters/postgres.ex | 38 +++++++ lib/arbor/tree.ex | 13 +-- mix.exs | 1 + mix.lock | 3 + test/arbor/adapters/postgres_test.exs | 43 ++++++++ test/arbor/parent_test.exs | 6 +- test/support/comment.ex | 1 + test/support/folder.ex | 1 + test/support/foreign.ex | 1 + test/support/repo.ex | 1 + test/test_helper.exs | 1 + 12 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 .credo.exs diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..a4c3434 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,153 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any exec using `mix credo -C `. If no exec name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: ["lib/", "src/", "test/", "web/", "apps/"], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.VariableNames, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapInto, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.PipeChainStart, + [excluded_argument_types: [:atom, :binary, :fn, :keyword], excluded_functions: []]}, + {Credo.Check.Refactor.UnlessWithElse, []}, + + # + ## Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + + # + # Controversial and experimental checks (opt-in, just remove `, false`) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + {Credo.Check.Design.DuplicatedCode, false}, + {Credo.Check.Readability.Specs}, + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.DoubleBooleanNegation, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Warning.UnsafeToAtom, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/lib/arbor/adapters/postgres.ex b/lib/arbor/adapters/postgres.ex index f3e1790..17c50b9 100644 --- a/lib/arbor/adapters/postgres.ex +++ b/lib/arbor/adapters/postgres.ex @@ -6,6 +6,8 @@ defmodule Arbor.Adapters.Postgres do import Ecto.Query @doc """ + Query for root level records. + ## Examples Basic Usage: iex> Arbor.Adapters.Postgres.roots(Arbor.Comment) @@ -29,4 +31,40 @@ defmodule Arbor.Adapters.Postgres do foreign_key = Keyword.get(opts, :foreign_key, :parent_id) from(t in schema, where: is_nil(field(t, ^foreign_key))) end + + @doc """ + Query for a child record's parent. + + ## Examples + TODO: Add examples + """ + @spec parent(struct(), Keyword.t()) :: Ecto.Query.t() + def parent(%{__meta__: meta} = child_struct, opts \\ []) do + schema = meta.schema + defaults = schema_defaults(schema) + merged_opts = Keyword.merge(defaults, opts) + + primary_key = merged_opts[:primary_key] + foreign_key = merged_opts[:foreign_key] + foreign_key_type = merged_opts[:foreign_key_type] + foreign_key_value = Map.get(child_struct, foreign_key) + + from( + t in schema, + where: field(t, ^primary_key) == type(^foreign_key_value, ^foreign_key_type) + ) + end + + # Generates defaults to be merged with opts given an ecto schema + defp schema_defaults(module) do + primary_key = :primary_key |> module.__schema__() |> List.first() + primary_key_type = module.__schema__(:type, primary_key) + + [ + primary_key: primary_key, + primary_key_type: primary_key_type, + foreign_key: :parent_id, + foreign_key_type: primary_key_type + ] + end end diff --git a/lib/arbor/tree.ex b/lib/arbor/tree.ex index dad04b2..ab9dc73 100644 --- a/lib/arbor/tree.ex +++ b/lib/arbor/tree.ex @@ -80,13 +80,12 @@ defmodule Arbor.Tree do end def parent(struct) do - from( - t in unquote(definition), - where: - fragment( - unquote("#{opts[:primary_key]} = ?"), - type(^struct.unquote(opts[:foreign_key]), unquote(opts[:foreign_key_type])) - ) + o = unquote(opts) + + Arbor.Adapters.Postgres.parent(struct, + foreign_key: o[:foreign_key], + parent_key: o[:parent_key], + foreign_key_type: o[:foreign_key_type] ) end diff --git a/mix.exs b/mix.exs index 03dfc68..7639b1f 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,7 @@ defmodule Arbor.Mixfile do {:postgrex, ">= 0.0.0"}, ## Test / Dev + {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, {:mix_test_watch, "~> 1.0", only: :dev, runtime: false}, {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, {:earmark, "~> 1.1", only: [:docs, :dev]}, diff --git a/mix.lock b/mix.lock index f4b351b..a7ebd2b 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,7 @@ %{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, + "credo": {:hex, :credo, "1.0.5", "fdea745579f8845315fe6a3b43e2f9f8866839cfbc8562bb72778e9fdaa94214", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, @@ -8,6 +10,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.21.1", "5ac36660846967cd869255f4426467a11672fec3d8db602c429425ce5b613b90", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/arbor/adapters/postgres_test.exs b/test/arbor/adapters/postgres_test.exs index 8839ffa..ada3365 100644 --- a/test/arbor/adapters/postgres_test.exs +++ b/test/arbor/adapters/postgres_test.exs @@ -73,4 +73,47 @@ defmodule Arbor.Adapters.PostgresTest do assert sorted_roots == [chauncys_home, rauls_home] end end + + describe "parent/1" do + test "given a child struct with an integer primary key, returns it's parent" do + [_, branch1, leaf1, _, _, _] = create_conversation("dogs") + + query = PGAdapter.parent(leaf1) + parent = Repo.one(query) + + assert parent == branch1 + end + + test "given a child struct with a UUID primary key, returns it's parent" do + root = create_folder("chauncy") + docs = create_folder("Documents", parent: root) + downloads = create_folder("Downloads", parent: root) + + create_folder("resumes", parent: docs) + create_folder("taxes", parent: docs) + create_folder("movies", parent: downloads) + + query = PGAdapter.parent(downloads) + parent = Repo.one(query) + + assert parent == root + end + end + + describe "parent/2" do + test "given a child struct with an alternate parent foreign key name, returns a root node query" do + root = create_foreign("chauncy") + docs = create_foreign("Documents", parent: root) + downloads = create_foreign("Downloads", parent: root) + + create_foreign("resumes", parent: docs) + create_foreign("taxes", parent: docs) + create_foreign("movies", parent: downloads) + + query = PGAdapter.parent(downloads, foreign_key: :parent_uuid) + parent = Repo.one(query) + + assert parent == root + end + end end diff --git a/test/arbor/parent_test.exs b/test/arbor/parent_test.exs index e7c324a..afe490d 100644 --- a/test/arbor/parent_test.exs +++ b/test/arbor/parent_test.exs @@ -2,7 +2,7 @@ defmodule Arbor.ParentTest do use Arbor.TestCase describe "parent/1 with an integer PK" do - test "given a struct w/ returns it's children" do + test "given a struct w/ returns it's parent" do [_, branch1, leaf1, _, _, _] = create_chatter("pupperinos") parent = @@ -15,7 +15,7 @@ defmodule Arbor.ParentTest do end describe "parent/1 with a UUID PK" do - test "given a struct w/ returns it's children" do + test "given a struct w/ returns it's parent" do root = create_folder("chauncy") docs = create_folder("Documents", parent: root) downloads = create_folder("Downloads", parent: root) @@ -34,7 +34,7 @@ defmodule Arbor.ParentTest do end describe "parent/1 with a UUID PK and other than id column name" do - test "given a struct w/ returns it's children" do + test "given a struct w/ returns it's parent" do root = create_foreign("chauncy") docs = create_foreign("Documents", parent: root) downloads = create_foreign("Downloads", parent: root) diff --git a/test/support/comment.ex b/test/support/comment.ex index d405e4f..3201fa2 100644 --- a/test/support/comment.ex +++ b/test/support/comment.ex @@ -1,3 +1,4 @@ +# credo:disable-for-this-file defmodule Arbor.Comment do @moduledoc false use Ecto.Schema diff --git a/test/support/folder.ex b/test/support/folder.ex index 80c08e7..cc64405 100644 --- a/test/support/folder.ex +++ b/test/support/folder.ex @@ -1,3 +1,4 @@ +# credo:disable-for-this-file defmodule Arbor.Folder do @moduledoc false use Ecto.Schema diff --git a/test/support/foreign.ex b/test/support/foreign.ex index 1d79c5c..37a5200 100644 --- a/test/support/foreign.ex +++ b/test/support/foreign.ex @@ -1,3 +1,4 @@ +# credo:disable-for-this-file defmodule Arbor.Foreign do @moduledoc false use Ecto.Schema diff --git a/test/support/repo.ex b/test/support/repo.ex index 463b2e7..e7ab78f 100644 --- a/test/support/repo.ex +++ b/test/support/repo.ex @@ -1,3 +1,4 @@ +# credo:disable-for-this-file defmodule Arbor.Repo do @moduledoc false use Ecto.Repo, diff --git a/test/test_helper.exs b/test/test_helper.exs index 3c70b87..a403b5d 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,4 @@ +# credo:disable-for-this-file defmodule Arbor.TestCase do use ExUnit.CaseTemplate