Skip to content

Commit

Permalink
improvement: allow specifying multi-column foreign keys
Browse files Browse the repository at this point in the history
* improvement: add match_with option on references
* improvement: add match_type option on references
  • Loading branch information
rbino committed Nov 18, 2023
1 parent 0822109 commit 0649243
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 12 deletions.
3 changes: 2 additions & 1 deletion lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,8 @@ defmodule AshPostgres.DataLayer do
transformers: [
AshPostgres.Transformers.ValidateReferences,
AshPostgres.Transformers.EnsureTableOrPolymorphic,
AshPostgres.Transformers.PreventMultidimensionalArrayAggregates
AshPostgres.Transformers.PreventMultidimensionalArrayAggregates,
AshPostgres.Transformers.PreventAttributeMultitenancyAndNonFullMatchType
]

def migrate(args) do
Expand Down
16 changes: 16 additions & 0 deletions lib/migration_generator/migration_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -707,13 +707,18 @@ defmodule AshPostgres.MigrationGenerator do
primary_key?: merge_uniq!(references, table, :primary_key?, name),
on_delete: merge_uniq!(references, table, :on_delete, name),
on_update: merge_uniq!(references, table, :on_update, name),
match_with: merge_uniq!(references, table, :match_with, name) |> to_map(),
match_type: merge_uniq!(references, table, :match_type, name),
name: merge_uniq!(references, table, :name, name),
table: merge_uniq!(references, table, :table, name),
schema: merge_uniq!(references, table, :schema, name)
}
end
end

defp to_map(nil), do: nil
defp to_map(kw_list) when is_list(kw_list), do: Map.new(kw_list)

defp merge_uniq!(references, table, field, attribute) do
references
|> Enum.map(&Map.get(&1, field))
Expand Down Expand Up @@ -2675,6 +2680,8 @@ defmodule AshPostgres.MigrationGenerator do
multitenancy: multitenancy(relationship.destination),
on_delete: configured_reference.on_delete,
on_update: configured_reference.on_update,
match_with: configured_reference.match_with,
match_type: configured_reference.match_type,
name: configured_reference.name,
primary_key?: destination_attribute.primary_key?,
schema:
Expand All @@ -2700,6 +2707,8 @@ defmodule AshPostgres.MigrationGenerator do
|> Kernel.||(%{
on_delete: nil,
on_update: nil,
match_with: nil,
match_type: nil,
deferrable: false,
schema:
relationship.context[:data_layer][:schema] ||
Expand Down Expand Up @@ -3027,6 +3036,13 @@ defmodule AshPostgres.MigrationGenerator do
|> Map.put_new(:on_update, nil)
|> Map.update!(:on_delete, &(&1 && String.to_atom(&1)))
|> Map.update!(:on_update, &(&1 && String.to_atom(&1)))
|> Map.put_new(:match_with, nil)
|> Map.put_new(:match_type, nil)
|> Map.update!(
:match_with,
&(&1 && Enum.into(&1, %{}, fn {k, v} -> {String.to_atom(k), String.to_atom(v)} end))
)
|> Map.update!(:match_type, &(&1 && String.to_atom(&1)))
|> Map.put(
:name,
Map.get(references, :name) || "#{table}_#{attribute.source}_fkey"
Expand Down
83 changes: 73 additions & 10 deletions lib/migration_generator/operation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,54 @@ defmodule AshPostgres.MigrationGenerator.Operation do
def reference_type(%{type: type}, _) do
type
end

def with_match(reference, source_attribute \\ nil)

def with_match(
%{
primary_key?: false,
destination_attribute: reference_attribute,
multitenancy: %{strategy: :attribute, attribute: destination_attribute}
} = reference,
source_attribute
)
when not is_nil(source_attribute) and reference_attribute != destination_attribute do
with_targets =
[{as_atom(source_attribute), as_atom(destination_attribute)}]
|> Enum.into(reference.match_with || %{})
|> with_targets()

# We can only have match: :full here, this gets validated by a Transformer
join([with_targets, "match: :full"])
end

def with_match(reference, _) do
with_targets = with_targets(reference.match_with)
match_type = match_type(reference.match_type)

if with_targets != nil or match_type != nil do
join([with_targets, match_type])
else
nil
end
end

def with_targets(targets) when is_map(targets) do
targets_string =
targets
|> Enum.map(fn {source, destination} -> "#{source}: :#{destination}" end)
|> Enum.join(", ")

"with: [#{targets_string}]"
end

def with_targets(_), do: nil

def match_type(type) when type in [:simple, :partial, :full] do
"match: :#{type}"
end

def match_type(_), do: nil
end

defmodule CreateTable do
Expand All @@ -88,14 +136,11 @@ defmodule AshPostgres.MigrationGenerator.Operation do
table: table,
destination_attribute: reference_attribute,
schema: destination_schema,
multitenancy: %{strategy: :attribute, attribute: destination_attribute}
multitenancy: %{strategy: :attribute}
} = reference
} = attribute
}) do
with_match =
if !reference.primary_key? && destination_attribute != reference_attribute do
"with: [#{as_atom(source_attribute)}: :#{as_atom(destination_attribute)}], match: :full"
end
with_match = with_match(reference, source_attribute)

size =
if attribute[:size] do
Expand Down Expand Up @@ -136,6 +181,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = reference
} = attribute
}) do
with_match = with_match(reference)

size =
if attribute[:size] do
"size: #{attribute[:size]}"
Expand All @@ -146,6 +193,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
"references(:#{as_atom(table)}",
[
"column: #{inspect(destination_attribute)}",
with_match,
option("prefix", destination_schema),
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
Expand Down Expand Up @@ -198,6 +246,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = reference
} = attribute
}) do
with_match = with_match(reference)

size =
if attribute[:size] do
"size: #{attribute[:size]}"
Expand All @@ -208,6 +258,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
"references(:#{as_atom(table)}",
[
"column: #{inspect(destination_attribute)}",
with_match,
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
"prefix: prefix()",
Expand Down Expand Up @@ -236,6 +287,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = reference
} = attribute
}) do
with_match = with_match(reference)

size =
if attribute[:size] do
"size: #{attribute[:size]}"
Expand All @@ -251,6 +304,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
"references(:#{as_atom(table)}",
[
"column: #{inspect(destination_attribute)}",
with_match,
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
option("prefix", destination_schema),
Expand All @@ -277,6 +331,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = reference
} = attribute
}) do
with_match = with_match(reference)

size =
if attribute[:size] do
"size: #{attribute[:size]}"
Expand All @@ -287,6 +343,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
"references(:#{as_atom(table)}",
[
"column: #{inspect(destination_attribute)}",
with_match,
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
option("prefix", destination_schema),
Expand Down Expand Up @@ -449,13 +506,16 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = attribute,
_schema
) do
with_match = with_match(reference)

size =
if attribute[:size] do
"size: #{attribute[:size]}"
end

join([
"references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}",
with_match,
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
size,
Expand All @@ -471,7 +531,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
%{
references:
%{
multitenancy: %{strategy: :attribute, attribute: destination_attribute},
multitenancy: %{strategy: :attribute},
table: table,
schema: destination_schema,
destination_attribute: reference_attribute
Expand All @@ -484,10 +544,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
destination_schema
end

with_match =
if !reference.primary_key? && destination_attribute != reference_attribute do
"with: [#{as_atom(source_attribute)}: :#{as_atom(destination_attribute)}], match: :full"
end
with_match = with_match(reference, source_attribute)

size =
if attribute[:size] do
Expand Down Expand Up @@ -519,6 +576,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = attribute,
schema
) do
with_match = with_match(reference)

size =
if attribute[:size] do
"size: #{attribute[:size]}"
Expand All @@ -531,6 +590,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do

join([
"references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}",
with_match,
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
size,
Expand All @@ -553,6 +613,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
} = attribute,
schema
) do
with_match = with_match(reference)

destination_schema =
if schema != destination_schema do
destination_schema
Expand All @@ -565,6 +627,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do

join([
"references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}",
with_match,
"name: #{inspect(reference.name)}",
"type: #{inspect(reference_type(attribute, reference))}",
size,
Expand Down
20 changes: 19 additions & 1 deletion lib/reference.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
defmodule AshPostgres.Reference do
@moduledoc "Represents the configuration of a reference (i.e foreign key)."
defstruct [:relationship, :on_delete, :on_update, :name, :deferrable, ignore?: false]
defstruct [
:relationship,
:on_delete,
:on_update,
:name,
:match_with,
:match_type,
:deferrable,
ignore?: false
]

def schema do
[
Expand Down Expand Up @@ -37,6 +46,15 @@ defmodule AshPostgres.Reference do
type: :string,
doc:
"The name of the foreign key to generate in the database. Defaults to <table>_<source_attribute>_fkey"
],
match_with: [
type: :non_empty_keyword_list,
doc:
"Defines additional keys to the foreign key in order to build a composite foreign key. The key should be the name of the source attribute (in the current resource), the value the name of the destination attribute."
],
match_type: [
type: {:one_of, [:simple, :partial, :full]},
doc: "select if the match is `:simple`, `:partial`, or `:full`"
]
]
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule AshPostgres.Transformers.PreventAttributeMultitenancyAndNonFullMatchType do
@moduledoc false
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer

def transform(dsl) do
if Transformer.get_option(dsl, [:multitenancy], :strategy) == :attribute do
dsl
|> AshPostgres.DataLayer.Info.references()
|> Enum.filter(&(&1.match_type && &1.match_type != :full))
|> Enum.each(fn reference ->
relationship = Ash.Resource.Info.relationship(dsl, reference.relationship)

if uses_attribute_strategy?(relationship) and
not targets_primary_key?(relationship) and
not targets_multitenancy_attribute?(relationship) do
resource = Transformer.get_persisted(dsl, :module)

raise Spark.Error.DslError,
module: resource,
message: """
Unsupported match_type.
The reference #{inspect(resource)}.#{reference.relationship} can't have `match_type: :#{reference.match_type}` because it's referencing another multitenant resource with attribute strategy using a non-primary key index, which requires using `match_type: :full`.
""",
path: [:postgres, :references, reference.relationship]
else
:ok
end
end)
else
{:ok, dsl}
end
end

defp uses_attribute_strategy?(relationship) do
Ash.Resource.Info.multitenancy_strategy(relationship.destination) == :attribute
end

defp targets_primary_key?(relationship) do
Ash.Resource.Info.attribute(
relationship.destination,
relationship.destination_attribute
)
|> Map.fetch!(:primary_key?)
end

defp targets_multitenancy_attribute?(relationship) do
relationship.destination_attribute ==
Ash.Resource.Info.multitenancy_attribute(relationship.destination)
end
end
Loading

0 comments on commit 0649243

Please sign in to comment.