Skip to content

Commit

Permalink
improvement: allow specifying multi-column foreign keys (#180)
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 authored Nov 20, 2023
1 parent fcda627 commit 021b7e4
Show file tree
Hide file tree
Showing 9 changed files with 420 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ spark_locals_without_parens = [
include: 1,
index: 1,
index: 2,
match_type: 1,
match_with: 1,
message: 1,
migrate?: 1,
migration_defaults: 1,
Expand Down
40 changes: 40 additions & 0 deletions documentation/dsls/DSL:-AshPostgres.DataLayer.cheatmd
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,46 @@ reference :post, on_delete: :delete, on_update: :update, name: "comments_to_post
</td>
</tr>

<tr>
<td style="text-align: left">
<a id="postgres-references-reference-match_with" href="#postgres-references-reference-match_with">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
match_with
</span>
</a>

</td>
<td style="text-align: left">
<code class="inline">Keyword.t</code>
</td>
<td style="text-align: left">

</td>
<td style="text-align: left" colspan=2>
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.
</td>
</tr>

<tr>
<td style="text-align: left">
<a id="postgres-references-reference-match_type" href="#postgres-references-reference-match_type">
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
match_type
</span>
</a>

</td>
<td style="text-align: left">
<code class="inline">:simple | :partial | :full</code>
</td>
<td style="text-align: left">

</td>
<td style="text-align: left" colspan=2>
select if the match is `:simple`, `:partial`, or `:full`
</td>
</tr>

</tbody>
</table>

Expand Down
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 @@ -3029,6 +3038,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
82 changes: 72 additions & 10 deletions lib/migration_generator/operation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,53 @@ 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_join(", ", fn {source, destination} -> "#{source}: :#{destination}" end)

"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 +135,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 +180,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 +192,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 +245,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 +257,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 +286,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 +303,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 +330,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 +342,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 +505,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 +530,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 +543,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 +575,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 +589,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 +612,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 +626,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
Loading

0 comments on commit 021b7e4

Please sign in to comment.