@@ -35,7 +35,7 @@ defmodule Ecto.Association do
35
35
required ( :cardinality ) => :one | :many ,
36
36
required ( :relationship ) => :parent | :child ,
37
37
required ( :owner ) => atom ,
38
- required ( :owner_key ) => atom ,
38
+ required ( :owner_key ) => list ( atom ) ,
39
39
required ( :field ) => atom ,
40
40
required ( :unique ) => boolean ,
41
41
optional ( atom ) => any }
@@ -71,7 +71,8 @@ defmodule Ecto.Association do
71
71
72
72
* `:owner` - the owner module of the association
73
73
74
- * `:owner_key` - the key in the owner with the association value
74
+ * `:owner_key` - the key in the owner with the association value, or a
75
+ list of keys for composite keys
75
76
76
77
* `:relationship` - if the relationship to the specified schema is
77
78
of a `:child` or a `:parent`
@@ -235,8 +236,15 @@ defmodule Ecto.Association do
235
236
# for the final WHERE clause with values.
236
237
{ _ , query , _ , dest_out_key } = Enum . reduce ( joins , { source , query , counter , source . out_key } , fn curr_rel , { prev_rel , query , counter , _ } ->
237
238
related_queryable = curr_rel . schema
238
-
239
- next = join ( query , :inner , [ { src , counter } ] , dest in ^ related_queryable , on: field ( src , ^ prev_rel . out_key ) == field ( dest , ^ curr_rel . in_key ) )
239
+ # TODO remove this once all relations store keys in lists
240
+ in_keys = List . wrap ( curr_rel . in_key )
241
+ out_keys = List . wrap ( prev_rel . out_key )
242
+ next = query
243
+ # join on the first field of the foreign key
244
+ |> join ( :inner , [ { src , counter } ] , dest in ^ related_queryable , on: field ( src , ^ hd ( out_keys ) ) == field ( dest , ^ hd ( in_keys ) ) )
245
+ # add the rest of the foreign key fields, if any
246
+ |> composite_joins_query ( counter , counter + 1 , tl ( out_keys ) , tl ( in_keys ) )
247
+ # consider where clauses on assocs
240
248
|> combine_joins_query ( curr_rel . where , counter + 1 )
241
249
242
250
{ curr_rel , next , counter + 1 , curr_rel . out_key }
@@ -320,6 +328,16 @@ defmodule Ecto.Association do
320
328
end )
321
329
end
322
330
331
+ # TODO docs
332
+ def composite_joins_query ( query , _binding_src , _binding_dst , [ ] , [ ] ) do
333
+ query
334
+ end
335
+ def composite_joins_query ( query , binding_src , binding_dst , [ src_key | src_keys ] , [ dst_key | dst_keys ] ) do
336
+ # TODO
337
+ [ query , binding_src , binding_dst , [ src_key | src_keys ] , [ dst_key | dst_keys ] ] |> IO . inspect ( label: :composite_joins_query )
338
+ query
339
+ end
340
+
323
341
@ doc """
324
342
Add the default assoc query where clauses to a join.
325
343
@@ -335,6 +353,16 @@ defmodule Ecto.Association do
335
353
% { query | joins: joins ++ [ % { join_expr | on: % { join_on | expr: expr , params: params } } ] }
336
354
end
337
355
356
+ # TODO docs
357
+ def composite_assoc_query ( query , _binding_src , [ ] , [ ] ) do
358
+ query
359
+ end
360
+ def composite_assoc_query ( query , binding_dst , [ dst_key | dst_keys ] , [ value | values ] ) do
361
+ # TODO
362
+ [ query , binding_dst , [ dst_key | dst_keys ] , [ value | values ] ] |> IO . inspect ( label: :composite_assoc_query )
363
+ query
364
+ end
365
+
338
366
@ doc """
339
367
Add the default assoc query where clauses a provided query.
340
368
"""
@@ -632,6 +660,10 @@ defmodule Ecto.Association do
632
660
633
661
defp primary_key! ( nil ) , do: [ ]
634
662
defp primary_key! ( struct ) , do: Ecto . primary_key! ( struct )
663
+
664
+ def missing_fields ( queryable , related_key ) do
665
+ Enum . filter related_key , & is_nil ( queryable . __schema__ ( :type , & 1 ) )
666
+ end
635
667
end
636
668
637
669
defmodule Ecto.Association.Has do
@@ -644,8 +676,8 @@ defmodule Ecto.Association.Has do
644
676
* `field` - The name of the association field on the schema
645
677
* `owner` - The schema where the association was defined
646
678
* `related` - The schema that is associated
647
- * `owner_key` - The key on the `owner` schema used for the association
648
- * `related_key` - The key on the `related` schema used for the association
679
+ * `owner_key` - The list of columns that form the key on the `owner` schema used for the association
680
+ * `related_key` - The list of columns that form the key on the `related` schema used for the association
649
681
* `queryable` - The real query to use for querying association
650
682
* `on_delete` - The action taken on associations when schema is deleted
651
683
* `on_replace` - The action taken on associations when schema is replaced
@@ -673,8 +705,8 @@ defmodule Ecto.Association.Has do
673
705
{ :error , "associated schema #{ inspect queryable } does not exist" }
674
706
not function_exported? ( queryable , :__schema__ , 2 ) ->
675
707
{ :error , "associated module #{ inspect queryable } is not an Ecto schema" }
676
- is_nil queryable . __schema__ ( :type , related_key ) ->
677
- { :error , "associated schema #{ inspect queryable } does not have field `#{ related_key } `" }
708
+ [ ] != ( missing_fields = Ecto.Association . missing_fields ( queryable , related_key ) ) ->
709
+ { :error , "associated schema #{ inspect queryable } does not have field(s) `#{ inspect missing_fields } `" }
678
710
true ->
679
711
:ok
680
712
end
@@ -686,14 +718,17 @@ defmodule Ecto.Association.Has do
686
718
cardinality = Keyword . fetch! ( opts , :cardinality )
687
719
related = Ecto.Association . related_from_query ( queryable , name )
688
720
689
- ref =
721
+ refs =
690
722
module
691
723
|> Module . get_attribute ( :primary_key )
692
724
|> get_ref ( opts [ :references ] , name )
725
+ |> List . wrap ( )
693
726
694
- unless Module . get_attribute ( module , :ecto_fields ) [ ref ] do
695
- raise ArgumentError , "schema does not have the field #{ inspect ref } used by " <>
696
- "association #{ inspect name } , please set the :references option accordingly"
727
+ for ref <- refs do
728
+ unless Module . get_attribute ( module , :ecto_fields ) [ ref ] do
729
+ raise ArgumentError , "schema does not have the field #{ inspect ref } used by " <>
730
+ "association #{ inspect name } , please set the :references option accordingly"
731
+ end
697
732
end
698
733
699
734
if opts [ :through ] do
@@ -725,13 +760,19 @@ defmodule Ecto.Association.Has do
725
760
raise ArgumentError , "expected `:where` for #{ inspect name } to be a keyword list, got: `#{ inspect where } `"
726
761
end
727
762
763
+ foreign_key = case opts [ :foreign_key ] do
764
+ nil -> Enum . map ( refs , & Ecto.Association . association_key ( module , & 1 ) )
765
+ key when is_atom ( key ) -> [ key ]
766
+ keys when is_list ( keys ) -> keys
767
+ end
768
+
728
769
% __MODULE__ {
729
770
field: name ,
730
771
cardinality: cardinality ,
731
772
owner: module ,
732
773
related: related ,
733
- owner_key: ref ,
734
- related_key: opts [ : foreign_key] || Ecto.Association . association_key ( module , ref ) ,
774
+ owner_key: refs ,
775
+ related_key: foreign_key ,
735
776
queryable: queryable ,
736
777
on_delete: on_delete ,
737
778
on_replace: on_replace ,
@@ -756,19 +797,23 @@ defmodule Ecto.Association.Has do
756
797
757
798
@ impl true
758
799
def joins_query ( % { related_key: related_key , owner: owner , owner_key: owner_key , queryable: queryable } = assoc ) do
759
- from ( o in owner , join: q in ^ queryable , on: field ( q , ^ related_key ) == field ( o , ^ owner_key ) )
800
+ # TODO find out how to handle a dynamic list of fields here
801
+ from ( o in owner , join: q in ^ queryable , on: field ( q , ^ hd ( related_key ) ) == field ( o , ^ hd ( owner_key ) ) )
802
+ |> Ecto.Association . composite_joins_query ( 0 , 1 , tl ( related_key ) , tl ( owner_key ) )
760
803
|> Ecto.Association . combine_joins_query ( assoc . where , 1 )
761
804
end
762
805
763
806
@ impl true
764
807
def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , [ value ] ) do
765
- from ( x in ( query || queryable ) , where: field ( x , ^ related_key ) == ^ value )
808
+ from ( x in ( query || queryable ) , where: field ( x , ^ hd ( related_key ) ) == ^ hd ( value ) )
809
+ |> Ecto.Association . composite_assoc_query ( 0 , tl ( related_key ) , tl ( value ) )
766
810
|> Ecto.Association . combine_assoc_query ( assoc . where )
767
811
end
768
812
769
813
@ impl true
770
814
def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , values ) do
771
- from ( x in ( query || queryable ) , where: field ( x , ^ related_key ) in ^ values )
815
+ from ( x in ( query || queryable ) , where: field ( x , ^ hd ( related_key ) ) in ^ Enum . map ( values , & hd / 1 ) )
816
+ |> Ecto.Association . composite_assoc_query ( 0 , tl ( related_key ) , Enum . map ( values , & tl / 1 ) )
772
817
|> Ecto.Association . combine_assoc_query ( assoc . where )
773
818
end
774
819
@@ -807,16 +852,21 @@ defmodule Ecto.Association.Has do
807
852
% { data: parent , repo: repo } = parent_changeset
808
853
% { action: action , changes: changes } = changeset
809
854
810
- { key , value } = parent_key ( assoc , parent )
811
- changeset = update_parent_key ( changeset , action , key , value )
812
- changeset = Ecto.Association . update_parent_prefix ( changeset , parent )
855
+ parent_keys = parent_keys ( assoc , parent )
856
+ changeset = Enum . reduce parent_keys , changeset , fn { key , value } , changeset ->
857
+ changeset = update_parent_key ( changeset , action , key , value )
858
+ Ecto.Association . update_parent_prefix ( changeset , parent )
859
+ end
813
860
814
861
case apply ( repo , action , [ changeset , opts ] ) do
815
862
{ :ok , _ } = ok ->
816
863
if action == :delete , do: { :ok , nil } , else: ok
817
864
{ :error , changeset } ->
818
- original = Map . get ( changes , key )
819
- { :error , put_in ( changeset . changes [ key ] , original ) }
865
+ changeset = Enum . reduce parent_keys , changeset , fn { key , _ } , changeset ->
866
+ original = Map . get ( changes , key )
867
+ put_in ( changeset . changes [ key ] , original )
868
+ end
869
+ { :error , changeset }
820
870
end
821
871
end
822
872
@@ -825,11 +875,21 @@ defmodule Ecto.Association.Has do
825
875
defp update_parent_key ( changeset , _action , key , value ) ,
826
876
do: Ecto.Changeset . put_change ( changeset , key , value )
827
877
828
- defp parent_key ( % { related_key: related_key } , nil ) do
829
- { related_key , nil }
878
+ defp parent_keys ( % { related_key: related_keys } , nil ) when is_list ( related_keys ) do
879
+ Enum . map related_keys , fn related_key -> { related_key , nil } end
880
+ end
881
+ defp parent_keys ( % { related_key: related_key } , nil ) do
882
+ [ { related_key , nil } ]
883
+ end
884
+ defp parent_keys ( % { owner_key: owner_keys , related_key: related_keys } , owner ) when is_list ( owner_keys ) and is_list ( related_keys ) do
885
+ owner_keys
886
+ |> Enum . zip ( related_keys )
887
+ |> Enum . map ( fn { owner_key , related_key } ->
888
+ { related_key , Map . get ( owner , owner_key ) }
889
+ end )
830
890
end
831
- defp parent_key ( % { owner_key: owner_key , related_key: related_key } , owner ) do
832
- { related_key , Map . get ( owner , owner_key ) }
891
+ defp parent_keys ( % { owner_key: owner_key , related_key: related_key } , owner ) do
892
+ [ { related_key , Map . get ( owner , owner_key ) } ]
833
893
end
834
894
835
895
## Relation callbacks
@@ -982,16 +1042,16 @@ defmodule Ecto.Association.BelongsTo do
982
1042
{ :error , "associated schema #{ inspect queryable } does not exist" }
983
1043
not function_exported? ( queryable , :__schema__ , 2 ) ->
984
1044
{ :error , "associated module #{ inspect queryable } is not an Ecto schema" }
985
- is_nil queryable . __schema__ ( :type , related_key ) ->
986
- { :error , "associated schema #{ inspect queryable } does not have field `#{ related_key } `" }
1045
+ [ ] != ( missing_fields = Ecto.Association . missing_fields ( queryable , related_key ) ) ->
1046
+ { :error , "associated schema #{ inspect queryable } does not have field(s) `#{ inspect missing_fields } `" }
987
1047
true ->
988
1048
:ok
989
1049
end
990
1050
end
991
1051
992
1052
@ impl true
993
1053
def struct ( module , name , opts ) do
994
- ref = if ref = opts [ :references ] , do: ref , else: :id
1054
+ refs = if ref = opts [ :references ] , do: List . wrap ( ref ) , else: [ :id ]
995
1055
queryable = Keyword . fetch! ( opts , :queryable )
996
1056
related = Ecto.Association . related_from_query ( queryable , name )
997
1057
on_replace = Keyword . get ( opts , :on_replace , :raise )
@@ -1013,8 +1073,8 @@ defmodule Ecto.Association.BelongsTo do
1013
1073
field: name ,
1014
1074
owner: module ,
1015
1075
related: related ,
1016
- owner_key: Keyword . fetch! ( opts , :foreign_key ) ,
1017
- related_key: ref ,
1076
+ owner_key: List . wrap ( Keyword . fetch! ( opts , :foreign_key ) ) ,
1077
+ related_key: refs ,
1018
1078
queryable: queryable ,
1019
1079
on_replace: on_replace ,
1020
1080
defaults: defaults ,
@@ -1031,19 +1091,22 @@ defmodule Ecto.Association.BelongsTo do
1031
1091
1032
1092
@ impl true
1033
1093
def joins_query ( % { related_key: related_key , owner: owner , owner_key: owner_key , queryable: queryable } = assoc ) do
1034
- from ( o in owner , join: q in ^ queryable , on: field ( q , ^ related_key ) == field ( o , ^ owner_key ) )
1094
+ from ( o in owner , join: q in ^ queryable , on: field ( q , ^ hd ( related_key ) ) == field ( o , ^ hd ( owner_key ) ) )
1095
+ |> Ecto.Association . composite_joins_query ( 0 , 1 , tl ( related_key ) , tl ( owner_key ) )
1035
1096
|> Ecto.Association . combine_joins_query ( assoc . where , 1 )
1036
1097
end
1037
1098
1038
1099
@ impl true
1039
1100
def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , [ value ] ) do
1040
- from ( x in ( query || queryable ) , where: field ( x , ^ related_key ) == ^ value )
1101
+ from ( x in ( query || queryable ) , where: field ( x , ^ hd ( related_key ) ) == ^ hd ( value ) )
1102
+ |> Ecto.Association . composite_assoc_query ( 0 , tl ( related_key ) , tl ( value ) )
1041
1103
|> Ecto.Association . combine_assoc_query ( assoc . where )
1042
1104
end
1043
1105
1044
1106
@ impl true
1045
1107
def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , values ) do
1046
- from ( x in ( query || queryable ) , where: field ( x , ^ related_key ) in ^ values )
1108
+ from ( x in ( query || queryable ) , where: field ( x , ^ hd ( related_key ) ) in ^ Enum . map ( values , & hd / 1 ) )
1109
+ |> Ecto.Association . composite_assoc_query ( 0 , tl ( related_key ) , Enum . map ( values , & tl / 1 ) )
1047
1110
|> Ecto.Association . combine_assoc_query ( assoc . where )
1048
1111
end
1049
1112
@@ -1264,11 +1327,12 @@ defmodule Ecto.Association.ManyToMany do
1264
1327
1265
1328
owner_key_type = owner . __schema__ ( :type , owner_key )
1266
1329
1330
+ # TODO fix the hd(values)
1267
1331
# We only need to join in the "join table". Preload and Ecto.assoc expressions can then filter
1268
1332
# by &1.join_owner_key in ^... to filter down to the associated entries in the related table.
1269
1333
from ( q in ( query || queryable ) ,
1270
1334
join: j in ^ join_through , on: field ( q , ^ related_key ) == field ( j , ^ join_related_key ) ,
1271
- where: field ( j , ^ join_owner_key ) in type ( ^ values , { :in , ^ owner_key_type } )
1335
+ where: field ( j , ^ join_owner_key ) in type ( ^ hd ( values ) , { :in , ^ owner_key_type } )
1272
1336
)
1273
1337
|> Ecto.Association . combine_assoc_query ( assoc . where )
1274
1338
|> Ecto.Association . combine_joins_query ( assoc . join_where , 1 )
0 commit comments