Last active
September 29, 2020 14:45
Revisions
-
mgwidmann revised this gist
Nov 5, 2017 . 1 changed file with 19 additions and 6 deletions.There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -12,7 +12,7 @@ defmodule Ecto.Polymorphic do if check_polymorphic_options!(opts[:polymorphic], types) do {:module, module, _binary, _fns} = Ecto.Polymorphic.Type.generate_ecto_type(__CALLER__.module, name, types) quote do unquote(__MODULE__).__belongs_to__(__MODULE__, unquote(name), unquote(Keyword.put(opts, :types, types))) field unquote(:"#{name}_type"), unquote(module) end else @@ -40,7 +40,7 @@ defmodule Ecto.Polymorphic do ### COPIED FROM ECTO w/ small modifications :( ### @valid_belongs_to_options [:foreign_key, :references, :define_field, :type, :types, :on_replace, :defaults, :primary_key, :polymorphic] @doc false # def __belongs_to__(mod, name, queryable, opts) do @@ -102,7 +102,10 @@ defmodule Ecto.Polymorphic do @on_replace_opts [:raise, :mark_as_invalid, :delete, :nilify, :update] defstruct [:field, :owner, :owner_key, :related_key, :type_field, :on_cast, :key_field, :on_replace, defaults: [], cardinality: :one, relationship: :parent, unique: true, assoc_query_receives_structs: true, # unused but required by ecto queryable: nil ] @doc false def struct(module, name, opts) do @@ -121,10 +124,16 @@ defmodule Ecto.Polymorphic do key_field: :"#{name}_id", type_field: :"#{name}_type", on_replace: on_replace, defaults: opts[:defaults] || [], # Set something queryable to make ecto happy queryable: default_queryable(opts[:types]) } end @doc false defp default_queryable([{_db_value, module} | _]), do: module defp default_queryable([module | _]), do: module @doc false defdelegate build(refl, struct, attributes), to: Ecto.Association.BelongsTo @@ -143,16 +152,20 @@ defmodule Ecto.Polymorphic do """ end def preload(_refl, _repo, _query, []), do: [] def preload(%{type_field: type_field, key_field: key_field, owner: mod} = refl, repo, base_query, [%mod{} | _] = structs, opts) do structs |> Enum.map(&(Map.fetch!(&1, type_field))) |> Enum.uniq() |> Enum.flat_map(fn type -> values = Enum.filter(structs, &(Map.fetch!(&1, type_field) == type)) |> Enum.map(&(Map.fetch!(&1, key_field))) |> Enum.uniq() [related_key] = type.__schema__(:primary_key) query = from x in type, where: field(x, ^related_key) in ^values query = %{query | select: base_query.select, prefix: base_query.prefix} Ecto.Repo.Preloader.normalize_query(query, refl, {0, related_key}) |> case do list when is_list(list) -> list query -> repo.all(query, opts) -
mgwidmann revised this gist
Nov 4, 2017 . 1 changed file with 26 additions and 33 deletions.There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,21 +1,5 @@ defmodule Ecto.Polymorphic do defmacro __using__(_) do quote do require Ecto.Schema @@ -59,6 +43,7 @@ defmodule Ecto.Polymorphic do @valid_belongs_to_options [:foreign_key, :references, :define_field, :type, :on_replace, :defaults, :primary_key, :polymorphic] @doc false # def __belongs_to__(mod, name, queryable, opts) do def __belongs_to__(mod, name, opts) do check_options!(opts, @valid_belongs_to_options, "belongs_to/3") @@ -74,6 +59,7 @@ defmodule Ecto.Polymorphic do end struct = # association(mod, :one, name, Ecto.Association.BelongsTo, [queryable: queryable] ++ opts) association(mod, :one, name, Ecto.Polymorphic.Association.BelongsTo, opts) Module.put_attribute(mod, :changeset_fields, {name, {:assoc, struct}}) end @@ -150,35 +136,42 @@ defmodule Ecto.Polymorphic do """ end def assoc_query(_, _, _) do raise """ #{__MODULE__} Association Error: Polymorphic associations cannot return an association query! Convert to a concrete table. Perhaps you meant to use preload instead? """ end def preload(%{type_field: type_field, key_field: key_field, owner: mod} = refl, repo, nil, fields, prefix, [%mod{} | _] = structs, opts) do structs |> Enum.map(&(Map.fetch!(&1, type_field))) |> Enum.uniq() |> Enum.flat_map(fn type -> values = Enum.filter(structs, &(Map.fetch!(&1, type_field) == type)) |> Enum.map(&(Map.fetch!(&1, key_field))) |> Enum.uniq() [related_key] = type.__schema__(:primary_key) query = from x in type, where: field(x, ^related_key) in ^values Ecto.Repo.Preloader.fetch_query(structs, refl, query, prefix, {0, related_key}, fields) |> case do list when is_list(list) -> list query -> repo.all(query, opts) end end) end def preload_info(%{type_field: _type_field, owner: _mod} = refl) do {:assoc, refl, nil} end defdelegate on_repo_change(data, changeset, meta, opts), to: Ecto.Association.BelongsTo @doc false def after_compile_validation(_assoc, _env) do # TODO: Check stuff here :ok end ## Relation callbacks @behaviour Ecto.Changeset.Relation -
mgwidmann created this gist
Jul 11, 2017 .There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,211 @@ defmodule Ecto.Polymorphic do @moduledoc """ Supports polymorphic belong_to relationships. Example: defmodule MyApp.User do use Ecto.Schema schema "users" do use Ecto.Polymorphism belongs_to :addresses, ["BillingAddress": MyApp.BillingAddress, "ShippingAddress": MyApp.ShippingAddress], polymorphic: true end end """ defmacro __using__(_) do quote do require Ecto.Schema import Ecto.Schema, except: [belongs_to: 2, belongs_to: 3] import unquote(__MODULE__) end end defmacro belongs_to(name, types, opts \\ []) do if check_polymorphic_options!(opts[:polymorphic], types) do {:module, module, _binary, _fns} = Ecto.Polymorphic.Type.generate_ecto_type(__CALLER__.module, name, types) quote do unquote(__MODULE__).__belongs_to__(__MODULE__, unquote(name), unquote(opts)) field unquote(:"#{name}_type"), unquote(module) end else quote do Ecto.Schema.belongs_to(unquote(name), unquote(types), unquote(opts)) end end end def check_polymorphic_options!(nil, _types), do: nil def check_polymorphic_options!(false, _types), do: nil def check_polymorphic_options!(true, types) when not(is_nil(types) or length(types) == 0), do: true def check_polymorhpic_options!(_, _types) do raise """ Polymorphic relationships require knowing all the possible types at compile time. Pass them in as a keyword list mapping the expected database value to the Ecto Schema. Example: belongs_to :address, ["ShippingAddress": MyApp.ShippingAddress, "BillingAddress": MyApp.BillingAddress], polymophic: true Or if they're the same as the database value, just pass a list of Ecto Schemas: belongs_to :address, [MyApp.ShippingAddress, MyApp.BillingAddress], polymophic: true """ end ### COPIED FROM ECTO w/ small modifications :( ### @valid_belongs_to_options [:foreign_key, :references, :define_field, :type, :on_replace, :defaults, :primary_key, :polymorphic] @doc false def __belongs_to__(mod, name, opts) do check_options!(opts, @valid_belongs_to_options, "belongs_to/3") opts = Keyword.put_new(opts, :foreign_key, :"#{name}_id") foreign_key_type = opts[:type] || Module.get_attribute(mod, :foreign_key_type) if name == Keyword.get(opts, :foreign_key) do raise ArgumentError, "foreign_key #{inspect name} must be distinct from corresponding association name" end if Keyword.get(opts, :define_field, true) do Ecto.Schema.__field__(mod, opts[:foreign_key], foreign_key_type, opts) end struct = association(mod, :one, name, Ecto.Polymorphic.Association.BelongsTo, opts) Module.put_attribute(mod, :changeset_fields, {name, {:assoc, struct}}) end defp check_options!(opts, valid, fun_arity) do case Enum.find(opts, fn {k, _} -> not k in valid end) do {k, _} -> raise ArgumentError, "invalid option #{inspect k} for #{fun_arity}" nil -> :ok end end defp association(mod, cardinality, name, association, opts) do not_loaded = %Ecto.Association.NotLoaded{__owner__: mod, __field__: name, __cardinality__: cardinality} put_struct_field(mod, name, not_loaded) opts = [cardinality: cardinality] ++ opts struct = association.struct(mod, name, opts) Module.put_attribute(mod, :ecto_assocs, {name, struct}) struct end defp put_struct_field(mod, name, assoc) do fields = Module.get_attribute(mod, :struct_fields) if List.keyfind(fields, name, 0) do raise ArgumentError, "field/association #{inspect name} is already set on schema" end Module.put_attribute(mod, :struct_fields, {name, assoc}) end ### END -- COPIED FROM ECTO :( ### defmodule Association.BelongsTo do import Ecto.Query @behaviour Ecto.Association @on_replace_opts [:raise, :mark_as_invalid, :delete, :nilify, :update] defstruct [:field, :owner, :owner_key, :related_key, :type_field, :on_cast, :key_field, :on_replace, defaults: [], cardinality: :one, relationship: :parent, unique: true, assoc_query_receives_structs: true] @doc false def struct(module, name, opts) do on_replace = Keyword.get(opts, :on_replace, :raise) unless on_replace in @on_replace_opts do raise ArgumentError, "invalid `:on_replace` option for #{inspect name}. " <> "The only valid options are: " <> Enum.map_join(@on_replace_opts, ", ", &"`#{inspect &1}`") end %__MODULE__{ field: name, owner: module, owner_key: Keyword.fetch!(opts, :foreign_key), key_field: :"#{name}_id", type_field: :"#{name}_type", on_replace: on_replace, defaults: opts[:defaults] || [] } end @doc false defdelegate build(refl, struct, attributes), to: Ecto.Association.BelongsTo @doc false def joins_query(_) do raise """ #{__MODULE__} Join Error: Polymorphic associations cannot be joined with! Convert to a concrete table. Perhaps you meant to use preload instead? """ end @doc false def assoc_query(%{type_field: type_field, key_field: key_field, owner: mod}, nil, [struct = %mod{}]) do type = Map.fetch!(struct, type_field) value = Map.fetch!(struct, key_field) [related_key] = type.__schema__(:primary_key) from(x in type, where: field(x, ^related_key) == ^value) end def assoc_query(%{type_field: type_field, key_field: key_field, owner: mod} = refl, nil, [struct = %mod{} | _] = structs) do structs |> Enum.map(&(Map.fetch!(&1, type_field))) |> Enum.uniq() |> Enum.map(fn type -> values = Enum.filter_map(structs, &(Map.fetch!(&1, type_field) == type), &(Map.fetch!(&1, key_field))) [related_key] = type.__schema__(:primary_key) from x in type, where: field(x, ^related_key) in ^values end) end def preload_info(%{type_field: type_field, owner: mod} = refl, [%mod{} = struct | _]) do type = Map.fetch!(struct, type_field) [related_key] = type.__schema__(:primary_key) {:assoc, refl, {0, related_key}} end defdelegate on_repo_change(data, changeset, meta, opts), to: Ecto.Association.BelongsTo ## Relation callbacks @behaviour Ecto.Changeset.Relation defdelegate build(assoc), to: Ecto.Association.BelongsTo end defmodule Type do def generate_ecto_type(module, name, mapping) do module = Module.concat(module, :"#{name |> to_string() |> String.capitalize()}PolymorphicType") quoted = quote bind_quoted: [module: module, mapping: mapping] do @behaviour Ecto.Type def type(), do: :string for {db_value, schema} <- mapping do def cast(unquote(to_string(db_value))), do: {:ok, unquote(schema)} end def cast(_), do: :error def load(type), do: cast(type) for {db_value, schema} <- mapping do def dump(unquote(schema)), do: {:ok, unquote(to_string(db_value))} end def dump(_), do: :error end Module.create(module, quoted, Macro.Env.location(__ENV__)) end end end