Skip to content

Commit 021b7e4

Browse files
authored
improvement: allow specifying multi-column foreign keys (#180)
* improvement: add match_with option on references * improvement: add match_type option on references
1 parent fcda627 commit 021b7e4

File tree

9 files changed

+420
-12
lines changed

9 files changed

+420
-12
lines changed

.formatter.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ spark_locals_without_parens = [
1515
include: 1,
1616
index: 1,
1717
index: 2,
18+
match_type: 1,
19+
match_with: 1,
1820
message: 1,
1921
migrate?: 1,
2022
migration_defaults: 1,

documentation/dsls/DSL:-AshPostgres.DataLayer.cheatmd

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,46 @@ reference :post, on_delete: :delete, on_update: :update, name: "comments_to_post
11701170
</td>
11711171
</tr>
11721172

1173+
<tr>
1174+
<td style="text-align: left">
1175+
<a id="postgres-references-reference-match_with" href="#postgres-references-reference-match_with">
1176+
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
1177+
match_with
1178+
</span>
1179+
</a>
1180+
1181+
</td>
1182+
<td style="text-align: left">
1183+
<code class="inline">Keyword.t</code>
1184+
</td>
1185+
<td style="text-align: left">
1186+
1187+
</td>
1188+
<td style="text-align: left" colspan=2>
1189+
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.
1190+
</td>
1191+
</tr>
1192+
1193+
<tr>
1194+
<td style="text-align: left">
1195+
<a id="postgres-references-reference-match_type" href="#postgres-references-reference-match_type">
1196+
<span style="font-family: Inconsolata, Menlo, Courier, monospace;">
1197+
match_type
1198+
</span>
1199+
</a>
1200+
1201+
</td>
1202+
<td style="text-align: left">
1203+
<code class="inline">:simple | :partial | :full</code>
1204+
</td>
1205+
<td style="text-align: left">
1206+
1207+
</td>
1208+
<td style="text-align: left" colspan=2>
1209+
select if the match is `:simple`, `:partial`, or `:full`
1210+
</td>
1211+
</tr>
1212+
11731213
</tbody>
11741214
</table>
11751215

lib/data_layer.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,8 @@ defmodule AshPostgres.DataLayer do
392392
transformers: [
393393
AshPostgres.Transformers.ValidateReferences,
394394
AshPostgres.Transformers.EnsureTableOrPolymorphic,
395-
AshPostgres.Transformers.PreventMultidimensionalArrayAggregates
395+
AshPostgres.Transformers.PreventMultidimensionalArrayAggregates,
396+
AshPostgres.Transformers.PreventAttributeMultitenancyAndNonFullMatchType
396397
]
397398

398399
def migrate(args) do

lib/migration_generator/migration_generator.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,13 +707,18 @@ defmodule AshPostgres.MigrationGenerator do
707707
primary_key?: merge_uniq!(references, table, :primary_key?, name),
708708
on_delete: merge_uniq!(references, table, :on_delete, name),
709709
on_update: merge_uniq!(references, table, :on_update, name),
710+
match_with: merge_uniq!(references, table, :match_with, name) |> to_map(),
711+
match_type: merge_uniq!(references, table, :match_type, name),
710712
name: merge_uniq!(references, table, :name, name),
711713
table: merge_uniq!(references, table, :table, name),
712714
schema: merge_uniq!(references, table, :schema, name)
713715
}
714716
end
715717
end
716718

719+
defp to_map(nil), do: nil
720+
defp to_map(kw_list) when is_list(kw_list), do: Map.new(kw_list)
721+
717722
defp merge_uniq!(references, table, field, attribute) do
718723
references
719724
|> Enum.map(&Map.get(&1, field))
@@ -2675,6 +2680,8 @@ defmodule AshPostgres.MigrationGenerator do
26752680
multitenancy: multitenancy(relationship.destination),
26762681
on_delete: configured_reference.on_delete,
26772682
on_update: configured_reference.on_update,
2683+
match_with: configured_reference.match_with,
2684+
match_type: configured_reference.match_type,
26782685
name: configured_reference.name,
26792686
primary_key?: destination_attribute.primary_key?,
26802687
schema:
@@ -2700,6 +2707,8 @@ defmodule AshPostgres.MigrationGenerator do
27002707
|> Kernel.||(%{
27012708
on_delete: nil,
27022709
on_update: nil,
2710+
match_with: nil,
2711+
match_type: nil,
27032712
deferrable: false,
27042713
schema:
27052714
relationship.context[:data_layer][:schema] ||
@@ -3029,6 +3038,13 @@ defmodule AshPostgres.MigrationGenerator do
30293038
|> Map.put_new(:on_update, nil)
30303039
|> Map.update!(:on_delete, &(&1 && String.to_atom(&1)))
30313040
|> Map.update!(:on_update, &(&1 && String.to_atom(&1)))
3041+
|> Map.put_new(:match_with, nil)
3042+
|> Map.put_new(:match_type, nil)
3043+
|> Map.update!(
3044+
:match_with,
3045+
&(&1 && Enum.into(&1, %{}, fn {k, v} -> {String.to_atom(k), String.to_atom(v)} end))
3046+
)
3047+
|> Map.update!(:match_type, &(&1 && String.to_atom(&1)))
30323048
|> Map.put(
30333049
:name,
30343050
Map.get(references, :name) || "#{table}_#{attribute.source}_fkey"

lib/migration_generator/operation.ex

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,53 @@ defmodule AshPostgres.MigrationGenerator.Operation do
6666
def reference_type(%{type: type}, _) do
6767
type
6868
end
69+
70+
def with_match(reference, source_attribute \\ nil)
71+
72+
def with_match(
73+
%{
74+
primary_key?: false,
75+
destination_attribute: reference_attribute,
76+
multitenancy: %{strategy: :attribute, attribute: destination_attribute}
77+
} = reference,
78+
source_attribute
79+
)
80+
when not is_nil(source_attribute) and reference_attribute != destination_attribute do
81+
with_targets =
82+
[{as_atom(source_attribute), as_atom(destination_attribute)}]
83+
|> Enum.into(reference.match_with || %{})
84+
|> with_targets()
85+
86+
# We can only have match: :full here, this gets validated by a Transformer
87+
join([with_targets, "match: :full"])
88+
end
89+
90+
def with_match(reference, _) do
91+
with_targets = with_targets(reference.match_with)
92+
match_type = match_type(reference.match_type)
93+
94+
if with_targets != nil or match_type != nil do
95+
join([with_targets, match_type])
96+
else
97+
nil
98+
end
99+
end
100+
101+
def with_targets(targets) when is_map(targets) do
102+
targets_string =
103+
targets
104+
|> Enum.map_join(", ", fn {source, destination} -> "#{source}: :#{destination}" end)
105+
106+
"with: [#{targets_string}]"
107+
end
108+
109+
def with_targets(_), do: nil
110+
111+
def match_type(type) when type in [:simple, :partial, :full] do
112+
"match: :#{type}"
113+
end
114+
115+
def match_type(_), do: nil
69116
end
70117

71118
defmodule CreateTable do
@@ -88,14 +135,11 @@ defmodule AshPostgres.MigrationGenerator.Operation do
88135
table: table,
89136
destination_attribute: reference_attribute,
90137
schema: destination_schema,
91-
multitenancy: %{strategy: :attribute, attribute: destination_attribute}
138+
multitenancy: %{strategy: :attribute}
92139
} = reference
93140
} = attribute
94141
}) do
95-
with_match =
96-
if !reference.primary_key? && destination_attribute != reference_attribute do
97-
"with: [#{as_atom(source_attribute)}: :#{as_atom(destination_attribute)}], match: :full"
98-
end
142+
with_match = with_match(reference, source_attribute)
99143

100144
size =
101145
if attribute[:size] do
@@ -136,6 +180,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
136180
} = reference
137181
} = attribute
138182
}) do
183+
with_match = with_match(reference)
184+
139185
size =
140186
if attribute[:size] do
141187
"size: #{attribute[:size]}"
@@ -146,6 +192,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
146192
"references(:#{as_atom(table)}",
147193
[
148194
"column: #{inspect(destination_attribute)}",
195+
with_match,
149196
option("prefix", destination_schema),
150197
"name: #{inspect(reference.name)}",
151198
"type: #{inspect(reference_type(attribute, reference))}",
@@ -198,6 +245,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
198245
} = reference
199246
} = attribute
200247
}) do
248+
with_match = with_match(reference)
249+
201250
size =
202251
if attribute[:size] do
203252
"size: #{attribute[:size]}"
@@ -208,6 +257,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
208257
"references(:#{as_atom(table)}",
209258
[
210259
"column: #{inspect(destination_attribute)}",
260+
with_match,
211261
"name: #{inspect(reference.name)}",
212262
"type: #{inspect(reference_type(attribute, reference))}",
213263
"prefix: prefix()",
@@ -236,6 +286,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
236286
} = reference
237287
} = attribute
238288
}) do
289+
with_match = with_match(reference)
290+
239291
size =
240292
if attribute[:size] do
241293
"size: #{attribute[:size]}"
@@ -251,6 +303,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
251303
"references(:#{as_atom(table)}",
252304
[
253305
"column: #{inspect(destination_attribute)}",
306+
with_match,
254307
"name: #{inspect(reference.name)}",
255308
"type: #{inspect(reference_type(attribute, reference))}",
256309
option("prefix", destination_schema),
@@ -277,6 +330,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
277330
} = reference
278331
} = attribute
279332
}) do
333+
with_match = with_match(reference)
334+
280335
size =
281336
if attribute[:size] do
282337
"size: #{attribute[:size]}"
@@ -287,6 +342,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
287342
"references(:#{as_atom(table)}",
288343
[
289344
"column: #{inspect(destination_attribute)}",
345+
with_match,
290346
"name: #{inspect(reference.name)}",
291347
"type: #{inspect(reference_type(attribute, reference))}",
292348
option("prefix", destination_schema),
@@ -449,13 +505,16 @@ defmodule AshPostgres.MigrationGenerator.Operation do
449505
} = attribute,
450506
_schema
451507
) do
508+
with_match = with_match(reference)
509+
452510
size =
453511
if attribute[:size] do
454512
"size: #{attribute[:size]}"
455513
end
456514

457515
join([
458516
"references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}",
517+
with_match,
459518
"name: #{inspect(reference.name)}",
460519
"type: #{inspect(reference_type(attribute, reference))}",
461520
size,
@@ -471,7 +530,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
471530
%{
472531
references:
473532
%{
474-
multitenancy: %{strategy: :attribute, attribute: destination_attribute},
533+
multitenancy: %{strategy: :attribute},
475534
table: table,
476535
schema: destination_schema,
477536
destination_attribute: reference_attribute
@@ -484,10 +543,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
484543
destination_schema
485544
end
486545

487-
with_match =
488-
if !reference.primary_key? && destination_attribute != reference_attribute do
489-
"with: [#{as_atom(source_attribute)}: :#{as_atom(destination_attribute)}], match: :full"
490-
end
546+
with_match = with_match(reference, source_attribute)
491547

492548
size =
493549
if attribute[:size] do
@@ -519,6 +575,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
519575
} = attribute,
520576
schema
521577
) do
578+
with_match = with_match(reference)
579+
522580
size =
523581
if attribute[:size] do
524582
"size: #{attribute[:size]}"
@@ -531,6 +589,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
531589

532590
join([
533591
"references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}",
592+
with_match,
534593
"name: #{inspect(reference.name)}",
535594
"type: #{inspect(reference_type(attribute, reference))}",
536595
size,
@@ -553,6 +612,8 @@ defmodule AshPostgres.MigrationGenerator.Operation do
553612
} = attribute,
554613
schema
555614
) do
615+
with_match = with_match(reference)
616+
556617
destination_schema =
557618
if schema != destination_schema do
558619
destination_schema
@@ -565,6 +626,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do
565626

566627
join([
567628
"references(:#{as_atom(table)}, column: #{inspect(destination_attribute)}",
629+
with_match,
568630
"name: #{inspect(reference.name)}",
569631
"type: #{inspect(reference_type(attribute, reference))}",
570632
size,

lib/reference.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
defmodule AshPostgres.Reference do
22
@moduledoc "Represents the configuration of a reference (i.e foreign key)."
3-
defstruct [:relationship, :on_delete, :on_update, :name, :deferrable, ignore?: false]
3+
defstruct [
4+
:relationship,
5+
:on_delete,
6+
:on_update,
7+
:name,
8+
:match_with,
9+
:match_type,
10+
:deferrable,
11+
ignore?: false
12+
]
413

514
def schema do
615
[
@@ -37,6 +46,15 @@ defmodule AshPostgres.Reference do
3746
type: :string,
3847
doc:
3948
"The name of the foreign key to generate in the database. Defaults to <table>_<source_attribute>_fkey"
49+
],
50+
match_with: [
51+
type: :non_empty_keyword_list,
52+
doc:
53+
"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."
54+
],
55+
match_type: [
56+
type: {:one_of, [:simple, :partial, :full]},
57+
doc: "select if the match is `:simple`, `:partial`, or `:full`"
4058
]
4159
]
4260
end

0 commit comments

Comments
 (0)