@@ -35,7 +35,7 @@ defmodule Ecto.Association do
3535 required ( :cardinality ) => :one | :many ,
3636 required ( :relationship ) => :parent | :child ,
3737 required ( :owner ) => atom ,
38- required ( :owner_key ) => atom | list ( atom ) ,
38+ required ( :owner_key ) => list ( atom ) ,
3939 required ( :field ) => atom ,
4040 required ( :unique ) => boolean ,
4141 optional ( atom ) => any }
@@ -236,8 +236,15 @@ defmodule Ecto.Association do
236236 # for the final WHERE clause with values.
237237 { _ , query , _ , dest_out_key } = Enum . reduce ( joins , { source , query , counter , source . out_key } , fn curr_rel , { prev_rel , query , counter , _ } ->
238238 related_queryable = curr_rel . schema
239-
240- 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
241248 |> combine_joins_query ( curr_rel . where , counter + 1 )
242249
243250 { curr_rel , next , counter + 1 , curr_rel . out_key }
@@ -321,6 +328,16 @@ defmodule Ecto.Association do
321328 end )
322329 end
323330
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+
324341 @ doc """
325342 Add the default assoc query where clauses to a join.
326343
@@ -336,6 +353,16 @@ defmodule Ecto.Association do
336353 % { query | joins: joins ++ [ % { join_expr | on: % { join_on | expr: expr , params: params } } ] }
337354 end
338355
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+
339366 @ doc """
340367 Add the default assoc query where clauses a provided query.
341368 """
@@ -633,6 +660,10 @@ defmodule Ecto.Association do
633660
634661 defp primary_key! ( nil ) , do: [ ]
635662 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
636667end
637668
638669defmodule Ecto.Association.Has do
@@ -645,8 +676,8 @@ defmodule Ecto.Association.Has do
645676 * `field` - The name of the association field on the schema
646677 * `owner` - The schema where the association was defined
647678 * `related` - The schema that is associated
648- * `owner_key` - The key on the `owner` schema used for the association
649- * `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
650681 * `queryable` - The real query to use for querying association
651682 * `on_delete` - The action taken on associations when schema is deleted
652683 * `on_replace` - The action taken on associations when schema is replaced
@@ -674,8 +705,8 @@ defmodule Ecto.Association.Has do
674705 { :error , "associated schema #{ inspect queryable } does not exist" }
675706 not function_exported? ( queryable , :__schema__ , 2 ) ->
676707 { :error , "associated module #{ inspect queryable } is not an Ecto schema" }
677- is_nil queryable . __schema__ ( :type , related_key ) ->
678- { :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 } `" }
679710 true ->
680711 :ok
681712 end
@@ -687,12 +718,13 @@ defmodule Ecto.Association.Has do
687718 cardinality = Keyword . fetch! ( opts , :cardinality )
688719 related = Ecto.Association . related_from_query ( queryable , name )
689720
690- ref =
721+ refs =
691722 module
692723 |> Module . get_attribute ( :primary_key )
693724 |> get_ref ( opts [ :references ] , name )
725+ |> List . wrap ( )
694726
695- for ref <- List . wrap ( ref ) do
727+ for ref <- refs do
696728 unless Module . get_attribute ( module , :ecto_fields ) [ ref ] do
697729 raise ArgumentError , "schema does not have the field #{ inspect ref } used by " <>
698730 "association #{ inspect name } , please set the :references option accordingly"
@@ -728,13 +760,19 @@ defmodule Ecto.Association.Has do
728760 raise ArgumentError , "expected `:where` for #{ inspect name } to be a keyword list, got: `#{ inspect where } `"
729761 end
730762
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+
731769 % __MODULE__ {
732770 field: name ,
733771 cardinality: cardinality ,
734772 owner: module ,
735773 related: related ,
736- owner_key: ref ,
737- related_key: opts [ : foreign_key] || Ecto.Association . association_key ( module , ref ) ,
774+ owner_key: refs ,
775+ related_key: foreign_key ,
738776 queryable: queryable ,
739777 on_delete: on_delete ,
740778 on_replace: on_replace ,
@@ -759,19 +797,23 @@ defmodule Ecto.Association.Has do
759797
760798 @ impl true
761799 def joins_query ( % { related_key: related_key , owner: owner , owner_key: owner_key , queryable: queryable } = assoc ) do
762- 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 ) )
763803 |> Ecto.Association . combine_joins_query ( assoc . where , 1 )
764804 end
765805
766806 @ impl true
767807 def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , [ value ] ) do
768- 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 ) )
769810 |> Ecto.Association . combine_assoc_query ( assoc . where )
770811 end
771812
772813 @ impl true
773814 def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , values ) do
774- 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 ) )
775817 |> Ecto.Association . combine_assoc_query ( assoc . where )
776818 end
777819
@@ -1000,16 +1042,16 @@ defmodule Ecto.Association.BelongsTo do
10001042 { :error , "associated schema #{ inspect queryable } does not exist" }
10011043 not function_exported? ( queryable , :__schema__ , 2 ) ->
10021044 { :error , "associated module #{ inspect queryable } is not an Ecto schema" }
1003- is_nil queryable . __schema__ ( :type , related_key ) ->
1004- { :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 } `" }
10051047 true ->
10061048 :ok
10071049 end
10081050 end
10091051
10101052 @ impl true
10111053 def struct ( module , name , opts ) do
1012- ref = if ref = opts [ :references ] , do: ref , else: :id
1054+ refs = if ref = opts [ :references ] , do: List . wrap ( ref ) , else: [ :id ]
10131055 queryable = Keyword . fetch! ( opts , :queryable )
10141056 related = Ecto.Association . related_from_query ( queryable , name )
10151057 on_replace = Keyword . get ( opts , :on_replace , :raise )
@@ -1031,8 +1073,8 @@ defmodule Ecto.Association.BelongsTo do
10311073 field: name ,
10321074 owner: module ,
10331075 related: related ,
1034- owner_key: Keyword . fetch! ( opts , :foreign_key ) ,
1035- related_key: ref ,
1076+ owner_key: List . wrap ( Keyword . fetch! ( opts , :foreign_key ) ) ,
1077+ related_key: refs ,
10361078 queryable: queryable ,
10371079 on_replace: on_replace ,
10381080 defaults: defaults ,
@@ -1049,19 +1091,22 @@ defmodule Ecto.Association.BelongsTo do
10491091
10501092 @ impl true
10511093 def joins_query ( % { related_key: related_key , owner: owner , owner_key: owner_key , queryable: queryable } = assoc ) do
1052- 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 ) )
10531096 |> Ecto.Association . combine_joins_query ( assoc . where , 1 )
10541097 end
10551098
10561099 @ impl true
10571100 def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , [ value ] ) do
1058- 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 ) )
10591103 |> Ecto.Association . combine_assoc_query ( assoc . where )
10601104 end
10611105
10621106 @ impl true
10631107 def assoc_query ( % { related_key: related_key , queryable: queryable } = assoc , query , values ) do
1064- 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 ) )
10651110 |> Ecto.Association . combine_assoc_query ( assoc . where )
10661111 end
10671112
@@ -1282,11 +1327,12 @@ defmodule Ecto.Association.ManyToMany do
12821327
12831328 owner_key_type = owner . __schema__ ( :type , owner_key )
12841329
1330+ # TODO fix the hd(values)
12851331 # We only need to join in the "join table". Preload and Ecto.assoc expressions can then filter
12861332 # by &1.join_owner_key in ^... to filter down to the associated entries in the related table.
12871333 from ( q in ( query || queryable ) ,
12881334 join: j in ^ join_through , on: field ( q , ^ related_key ) == field ( j , ^ join_related_key ) ,
1289- 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 } )
12901336 )
12911337 |> Ecto.Association . combine_assoc_query ( assoc . where )
12921338 |> Ecto.Association . combine_joins_query ( assoc . join_where , 1 )
0 commit comments