diff --git a/CHANGELOG b/CHANGELOG index a9fc305aad..2039644e28 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ === HEAD +* Add association_join to model datasets, for setting up joins based on associations (jeremyevans) + * Add one_through_many association to many_through_many plugin, for only returning a single record (jeremyevans) * Add :graph_order association option, useful when :order needs to contain qualified identifiers (jeremyevans) diff --git a/README.rdoc b/README.rdoc index 4abd9cabea..b1e3f52444 100644 --- a/README.rdoc +++ b/README.rdoc @@ -747,6 +747,28 @@ You can dynamically customize eager loads for both +eager+ and +eager_graph+ whi # Eagerly load only replies containing 'foo', and the person and tags for those replies Post.eager(:replies=>{proc{|ds| ds.where(Sequel.like(text, '%foo%'))}=>[:person, :tags]}).all +=== Joining with Associations + +You can use the association_join method to add a join to the model's dataset based on the assocation: + + Post.association_join(:author) + # SELECT * FROM posts + # INNER JOIN authors AS author ON (author.id = posts.author_id) + +This comes with variants for different join types: + + Post.association_left_join(:replies) + # SELECT * FROM posts + # LEFT JOIN replies ON (replies.post_id = posts.id) + +Similar to the eager loading methods, you can use multiple associations and nested associations: + + Post.association_join(:author, :replies=>:person).all + # SELECT * FROM posts + # INNER JOIN authors AS author ON (author.id = posts.author_id) + # INNER JOIN replies ON (replies.post_id = posts.id) + # INNER JOIN people AS person ON (person.id = replies.person_id) + === Extending the underlying dataset The recommended way to implement table-wide logic by defining methods on the dataset using +dataset_module+: diff --git a/doc/association_basics.rdoc b/doc/association_basics.rdoc index 2ecdd22dd5..50846adca3 100644 --- a/doc/association_basics.rdoc +++ b/doc/association_basics.rdoc @@ -41,6 +41,12 @@ As is the code to add a related album to an artist: @artist.add_album(:name=>'RF') +It also makes it easier to creating queries that use joins based on the association: + + Artist.association_join(:albums) + # SELECT * FROM artists + # INNER JOIN albums ON (albums.artist_id = artists.id) + == The Types of Associations Sequel has five different association types built in: diff --git a/lib/sequel/dataset/graph.rb b/lib/sequel/dataset/graph.rb index 4d67a5fc03..1d2aef2583 100644 --- a/lib/sequel/dataset/graph.rb +++ b/lib/sequel/dataset/graph.rb @@ -76,8 +76,8 @@ def graph(dataset, join_conditions = nil, options = OPTS, &block) # Only allow table aliases that haven't been used raise_alias_error.call if @opts[:graph] && @opts[:graph][:table_aliases] && @opts[:graph][:table_aliases].include?(table_alias) - # Use a from_self if this is already a joined table - ds = (!@opts[:graph] && (@opts[:from].length > 1 || @opts[:join])) ? from_self(:alias=>options[:from_self_alias] || first_source) : self + # Use a from_self if this is already a joined table (or from_self specifically disabled for graphs) + ds = (@opts[:graph_from_self] != false && !@opts[:graph] && (@opts[:from].length > 1 || @opts[:join])) ? from_self(:alias=>options[:from_self_alias] || first_source) : self # Join the table early in order to avoid cloning the dataset twice ds = ds.join_table(options[:join_type] || :left_outer, table, join_conditions, :table_alias=>table_alias, :implicit_qualifier=>options[:implicit_qualifier], :qualify=>options[:qualify], &block) diff --git a/lib/sequel/model/associations.rb b/lib/sequel/model/associations.rb index 64ec0a555e..2188d84614 100644 --- a/lib/sequel/model/associations.rb +++ b/lib/sequel/model/associations.rb @@ -1263,7 +1263,7 @@ def one_to_one(name, opts=OPTS, &block) end Plugins.inherited_instance_variables(self, :@association_reflections=>:dup, :@autoreloading_associations=>:hash_dup, :@default_eager_limit_strategy=>nil) - Plugins.def_dataset_methods(self, [:eager, :eager_graph]) + Plugins.def_dataset_methods(self, [:eager, :eager_graph, :association_join, :association_full_join, :association_inner_join, :association_left_join, :association_right_join]) private @@ -1378,8 +1378,8 @@ def def_many_to_many(opts) jt_graph_block = opts[:graph_join_table_block] opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] - ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block) - ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block) + ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>ds.opts[:eager_graph][:join_type]||jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block) + ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, &graph_block) end def_association_dataset_methods(opts) @@ -1465,7 +1465,7 @@ def def_many_to_one(opts) graph_cks = opts[:graph_keys] opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] - ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) + ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) end def_association_dataset_methods(opts) @@ -1539,7 +1539,7 @@ def def_one_to_many(opts) graph_block = opts[:graph_block] opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] - ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) + ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal ds @@ -1961,6 +1961,26 @@ def set_one_to_one_associated_object(opts, o) # Artist.eager(:albums => {proc{|ds| ds.where{year > 1990}}=>{:tracks => :genre}}) module DatasetMethods Sequel::Dataset.def_mutation_method(:eager, :eager_graph, :module=>self) + + %w'inner left right full'.each do |type| + class_eval <x = y where +y+ is a Sequel::Model # instance, array of Sequel::Model instances, or a Sequel::Model dataset, @@ -2082,16 +2102,10 @@ def eager_graph(*associations) ds = clone(:eager_graph=>eg) ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations) else - # Each of the following have a symbol key for the table alias, with the following values: - # :reciprocals - the reciprocal instance variable to use for this association - # :reflections - AssociationReflection instance related to this association - # :requirements - array of requirements for this association - ds = clone(:eager_graph=>{:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0, :row_proc=>row_proc}) - ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations). - naked + new_eager_graph(associations) end end - + # Do not attempt to split the result set into associations, # just return results as simple objects. This is useful if you # want to use eager_graph as a shortcut to have all of the joins @@ -2182,8 +2196,25 @@ def eager_graph_build_associations(hashes) hashes.replace(EagerGraphLoader.new(self).load(hashes)) end + # Setup a new eager graph for the given associations and options + def new_eager_graph(associations, opts=OPTS) + # Each of the following have a symbol key for the table alias, with the following values: + # :reciprocals - the reciprocal instance variable to use for this association + # :reflections - AssociationReflection instance related to this association + # :requirements - array of requirements for this association + opts = {:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0, :row_proc=>row_proc}.merge(opts) + ds = clone(:eager_graph=>opts) + ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations).naked + end + private + # Return a new dataset with JOINs of the given type added, using the tables and + # conditions specified by the associations. + def _association_join(type, associations) + clone(:join=>clone(:graph_from_self=>false).new_eager_graph(associations, :join_type=>type).opts[:join]) + end + # If the association has conditions itself, then it requires additional filters be # added to the current dataset to ensure that the passed in object would also be # included by the association's conditions. diff --git a/lib/sequel/plugins/many_through_many.rb b/lib/sequel/plugins/many_through_many.rb index e42f1bfa14..1d2b765784 100644 --- a/lib/sequel/plugins/many_through_many.rb +++ b/lib/sequel/plugins/many_through_many.rb @@ -280,11 +280,11 @@ def def_many_through_many(opts) ds = eo[:self] iq = eo[:implicit_qualifier] opts.edges.each do |t| - ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :qualify=>:deep, :implicit_qualifier=>iq, &t[:block]) + ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>ds.opts[:eager_graph][:join_type]||t[:join_type], :qualify=>:deep, :implicit_qualifier=>iq, &t[:block]) iq = nil end fe = opts.final_edge - ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block) + ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, &graph_block) end def_association_dataset_methods(opts) diff --git a/lib/sequel/plugins/pg_array_associations.rb b/lib/sequel/plugins/pg_array_associations.rb index fa1de401db..c079a63d8e 100644 --- a/lib/sequel/plugins/pg_array_associations.rb +++ b/lib/sequel/plugins/pg_array_associations.rb @@ -302,7 +302,7 @@ def def_many_to_pg_array(opts) opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] - ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) + ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) ds end @@ -404,7 +404,7 @@ def def_pg_array_to_many(opts) opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] - ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) + ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) ds end diff --git a/spec/extensions/many_through_many_spec.rb b/spec/extensions/many_through_many_spec.rb index 362a7f1244..2e94f07711 100644 --- a/spec/extensions/many_through_many_spec.rb +++ b/spec/extensions/many_through_many_spec.rb @@ -792,6 +792,10 @@ def columns proc{@c1.eager_graph(Object.new)}.should raise_error(Sequel::Error) end + it "should support association_join" do + @c1.association_join(:tags).sql.should == "SELECT * FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) INNER JOIN albums ON (albums.id = albums_artists.album_id) INNER JOIN albums_tags ON (albums_tags.album_id = albums.id) INNER JOIN tags ON (tags.id = albums_tags.tag_id)" + end + it "should eagerly graph a single many_through_many association" do a = @c1.eager_graph(:tags).all a.should == [@c1.load(:id=>1)] @@ -1762,6 +1766,10 @@ def columns DB.sqls.length.should == 0 end + it "should support association_join" do + @c1.association_join(:tag).sql.should == "SELECT * FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) INNER JOIN albums ON (albums.id = albums_artists.album_id) INNER JOIN albums_tags ON (albums_tags.album_id = albums.id) INNER JOIN tags AS tag ON (tag.id = albums_tags.tag_id)" + end + it "should eagerly graph a single one_through_many association" do a = @c1.eager_graph(:tag).all a.should == [@c1.load(:id=>1)] diff --git a/spec/extensions/pg_array_associations_spec.rb b/spec/extensions/pg_array_associations_spec.rb index ee37768684..463888cba3 100644 --- a/spec/extensions/pg_array_associations_spec.rb +++ b/spec/extensions/pg_array_associations_spec.rb @@ -393,6 +393,11 @@ def id3 DB.sqls.should == [] end + it "should support association_join" do + @c1.association_join(:tags).sql.should == "SELECT * FROM artists INNER JOIN tags ON (artists.tag_ids @> ARRAY[tags.id])" + @c2.association_join(:artists).sql.should == "SELECT * FROM tags INNER JOIN artists ON (artists.tag_ids @> ARRAY[tags.id])" + end + it "should eagerly graph associations" do @c2.dataset._fetch = {:id=>2, :artists_id=>1, :tag_ids=>Sequel.pg_array([1,2,3])} @c1.dataset._fetch = {:id=>1, :tags_id=>2, :tag_ids=>Sequel.pg_array([1,2,3])} diff --git a/spec/integration/associations_test.rb b/spec/integration/associations_test.rb index 0f91a5a8eb..0cd90ae810 100644 --- a/spec/integration/associations_test.rb +++ b/spec/integration/associations_test.rb @@ -175,6 +175,84 @@ end shared_examples_for "filtering/excluding by associations" do + specify "should handle association inner joins" do + @Artist.association_join(:albums).all.should == [] + @Artist.association_join(:first_album).all.should == [] + @Album.association_join(:artist).all.should == [] + @Album.association_join(:tags).all.should == [] + @Album.association_join(:alias_tags).all.should == [] + @Tag.association_join(:albums).all.should == [] + unless @no_many_through_many + @Artist.association_join(:tags).all.should == [] + @Artist.association_join(:first_tag).all.should == [] + end + + @album.update(:artist => @artist) + @album.add_tag(@tag) + + @Artist.association_join(:albums).select_all(:artists).all.should == [@artist] + @Artist.association_join(:first_album).select_all(:artists).all.should == [@artist] + @Album.association_join(:artist).select_all(:albums).all.should == [@album] + @Album.association_join(:tags).select_all(:albums).all.should == [@album] + @Album.association_join(:alias_tags).select_all(:albums).all.should == [@album] + @Tag.association_join(:albums).select_all(:tags).all.should == [@tag] + unless @no_many_through_many + @Artist.association_join(:tags).select_all(:artists).all.should == [@artist] + @Artist.association_join(:first_tag).select_all(:artists).all.should == [@artist] + end + + @Artist.association_join(:albums).select_all(:albums).naked.all.should == [@album.values] + @Artist.association_join(:first_album).select_all(:first_album).naked.all.should == [@album.values] + @Album.association_join(:artist).select_all(:artist).naked.all.should == [@artist.values] + @Album.association_join(:tags).select_all(:tags).naked.all.should == [@tag.values] + @Album.association_join(:alias_tags).select_all(:alias_tags).naked.all.should == [@tag.values] + @Tag.association_join(:albums).select_all(:albums).naked.all.should == [@album.values] + unless @no_many_through_many + @Artist.association_join(:tags).select_all(:tags).naked.all.should == [@tag.values] + @Artist.association_join(:first_tag).select_all(:first_tag).naked.all.should == [@tag.values] + end + end + + specify "should handle association left joins" do + @Artist.association_left_join(:albums).select_all(:artists).all.should == [@artist] + @Artist.association_left_join(:first_album).select_all(:artists).all.should == [@artist] + @Album.association_left_join(:artist).select_all(:albums).all.should == [@album] + @Album.association_left_join(:tags).select_all(:albums).all.should == [@album] + @Album.association_left_join(:alias_tags).select_all(:albums).all.should == [@album] + @Tag.association_left_join(:albums).select_all(:tags).all.should == [@tag] + unless @no_many_through_many + @Artist.association_left_join(:tags).select_all(:artists).all.should == [@artist] + @Artist.association_left_join(:first_tag).select_all(:artists).all.should == [@artist] + end + + nil_hash = lambda{|obj| [obj.values.keys.inject({}){|h,k| h[k] = nil; h}]} + @Artist.association_left_join(:albums).select_all(:albums).naked.all.should == nil_hash[@album] + @Artist.association_left_join(:first_album).select_all(:first_album).naked.all.should == nil_hash[@album] + @Album.association_left_join(:artist).select_all(:artist).naked.all.should == nil_hash[@artist] + @Album.association_left_join(:tags).select_all(:tags).naked.all.should == nil_hash[@tag] + @Album.association_left_join(:alias_tags).select_all(:alias_tags).naked.all.should == nil_hash[@tag] + @Tag.association_left_join(:albums).select_all(:albums).naked.all.should == nil_hash[@album] + unless @no_many_through_many + @Artist.association_left_join(:tags).select_all(:tags).naked.all.should == nil_hash[@tag] + @Artist.association_left_join(:first_tag).select_all(:first_tag).naked.all.should == nil_hash[@tag] + end + + @album.update(:artist => @artist) + @album.add_tag(@tag) + + + @Artist.association_left_join(:albums).select_all(:albums).naked.all.should == [@album.values] + @Artist.association_left_join(:first_album).select_all(:first_album).naked.all.should == [@album.values] + @Album.association_left_join(:artist).select_all(:artist).naked.all.should == [@artist.values] + @Album.association_left_join(:tags).select_all(:tags).naked.all.should == [@tag.values] + @Album.association_left_join(:alias_tags).select_all(:alias_tags).naked.all.should == [@tag.values] + @Tag.association_left_join(:albums).select_all(:albums).naked.all.should == [@album.values] + unless @no_many_through_many + @Artist.association_left_join(:tags).select_all(:tags).naked.all.should == [@tag.values] + @Artist.association_left_join(:first_tag).select_all(:first_tag).naked.all.should == [@tag.values] + end + end + specify "should work correctly when filtering by associations" do @album.update(:artist => @artist) @album.add_tag(@tag) @@ -736,7 +814,6 @@ @Artist.exclude(:t_tag=>@Tag.filter(1=>0)).all.sort_by{|x| x.pk}.should == [@artist, artist] end end - end shared_examples_for "basic regular and composite key associations" do diff --git a/spec/model/eager_loading_spec.rb b/spec/model/eager_loading_spec.rb index 8c9684b7dc..0c154c887a 100644 --- a/spec/model/eager_loading_spec.rb +++ b/spec/model/eager_loading_spec.rb @@ -1015,6 +1015,21 @@ class ::GraphBandMember < Sequel::Model(:members) a.album.band.members.should == [GraphBandMember.load(:id => 5)] end + it "should set up correct inner joins when using association_join" do + GraphAlbum.association_join(:band).sql.should == 'SELECT * FROM albums INNER JOIN bands AS band ON (band.id = albums.band_id)' + GraphAlbum.association_join(:track).sql.should == 'SELECT * FROM albums INNER JOIN tracks AS track ON (track.album_id = albums.id)' + GraphAlbum.association_join(:tracks).sql.should == 'SELECT * FROM albums INNER JOIN tracks ON (tracks.album_id = albums.id)' + GraphAlbum.association_join(:genres).sql.should == 'SELECT * FROM albums INNER JOIN ag ON (ag.album_id = albums.id) INNER JOIN genres ON (genres.id = ag.genre_id)' + GraphAlbum.association_join(:genre).sql.should == 'SELECT * FROM albums INNER JOIN ag ON (ag.album_id = albums.id) INNER JOIN genres AS genre ON (genre.id = ag.genre_id)' + end + + it "should set up correct join types when using association_*_join" do + GraphAlbum.association_inner_join(:band).sql.should == 'SELECT * FROM albums INNER JOIN bands AS band ON (band.id = albums.band_id)' + GraphAlbum.association_left_join(:track).sql.should == 'SELECT * FROM albums LEFT JOIN tracks AS track ON (track.album_id = albums.id)' + GraphAlbum.association_right_join(:tracks).sql.should == 'SELECT * FROM albums RIGHT JOIN tracks ON (tracks.album_id = albums.id)' + GraphAlbum.association_full_join(:genres).sql.should == 'SELECT * FROM albums FULL JOIN ag ON (ag.album_id = albums.id) FULL JOIN genres ON (genres.id = ag.genre_id)' + end + it "should eagerly load a single many_to_one association" do ds = GraphAlbum.eager_graph(:band) ds.sql.should == 'SELECT albums.id, albums.band_id, band.id AS band_id_0, band.vocalist_id FROM albums LEFT OUTER JOIN bands AS band ON (band.id = albums.band_id)' @@ -1062,7 +1077,7 @@ class ::GraphBandMember < Sequel::Model(:members) a.first.genres.should == [GraphGenre.load(:id => 4)] end - it "should eagerly load a single many_to_many association" do + it "should eagerly load a single one_through_one association" do ds = GraphAlbum.eager_graph(:genre) ds.sql.should == 'SELECT albums.id, albums.band_id, genre.id AS genre_id FROM albums LEFT OUTER JOIN ag ON (ag.album_id = albums.id) LEFT OUTER JOIN genres AS genre ON (genre.id = ag.genre_id)' ds._fetch = {:id=>1, :band_id=>2, :genre_id=>4} @@ -1086,6 +1101,10 @@ class ::GraphBandMember < Sequel::Model(:members) c.eager_graph(:genres).sql.should == 'SELECT albums.id, albums.band_id, genres.id AS genres_id FROM albums LEFT OUTER JOIN ag AS genres_0 ON (genres_0.album_id = albums.id) LEFT OUTER JOIN genres ON (genres.id = genres_0.genre_id)' end + it "should handle multiple associations in a single call to association_join" do + GraphAlbum.association_join(:genres, :tracks, :band).sql.should == 'SELECT * FROM albums INNER JOIN ag ON (ag.album_id = albums.id) INNER JOIN genres ON (genres.id = ag.genre_id) INNER JOIN tracks ON (tracks.album_id = albums.id) INNER JOIN bands AS band ON (band.id = albums.band_id)' + end + it "should eagerly load multiple associations in a single call" do ds = GraphAlbum.eager_graph(:genres, :tracks, :band) ds.sql.should == 'SELECT albums.id, albums.band_id, genres.id AS genres_id, tracks.id AS tracks_id, tracks.album_id, band.id AS band_id_0, band.vocalist_id FROM albums LEFT OUTER JOIN ag ON (ag.album_id = albums.id) LEFT OUTER JOIN genres ON (genres.id = ag.genre_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums.id) LEFT OUTER JOIN bands AS band ON (band.id = albums.band_id)' @@ -1098,6 +1117,10 @@ class ::GraphBandMember < Sequel::Model(:members) a.genres.should == [GraphGenre.load(:id => 4)] end + it "should handle multiple associations in separate calls to association_join" do + GraphAlbum.association_join(:genres).association_join(:tracks).association_join(:band).sql.should == 'SELECT * FROM albums INNER JOIN ag ON (ag.album_id = albums.id) INNER JOIN genres ON (genres.id = ag.genre_id) INNER JOIN tracks ON (tracks.album_id = albums.id) INNER JOIN bands AS band ON (band.id = albums.band_id)' + end + it "should eagerly load multiple associations in separate calls" do ds = GraphAlbum.eager_graph(:genres).eager_graph(:tracks).eager_graph(:band) ds.sql.should == 'SELECT albums.id, albums.band_id, genres.id AS genres_id, tracks.id AS tracks_id, tracks.album_id, band.id AS band_id_0, band.vocalist_id FROM albums LEFT OUTER JOIN ag ON (ag.album_id = albums.id) LEFT OUTER JOIN genres ON (genres.id = ag.genre_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums.id) LEFT OUTER JOIN bands AS band ON (band.id = albums.band_id)' @@ -1110,6 +1133,15 @@ class ::GraphBandMember < Sequel::Model(:members) a.genres.should == [GraphGenre.load(:id => 4)] end + it "should handle cascading associations in a single call to association_join" do + GraphTrack.association_join(:album=>{:band=>:members}).sql.should == 'SELECT * FROM tracks INNER JOIN albums AS album ON (album.id = tracks.album_id) INNER JOIN bands AS band ON (band.id = album.band_id) INNER JOIN bm ON (bm.band_id = band.id) INNER JOIN members ON (members.id = bm.member_id)' + GraphBand.association_join({:albums=>:tracks}, :members).sql.should == 'SELECT * FROM bands INNER JOIN albums ON (albums.band_id = bands.id) INNER JOIN tracks ON (tracks.album_id = albums.id) INNER JOIN bm ON (bm.band_id = bands.id) INNER JOIN members ON (members.id = bm.member_id)' + end + + it "should handle matching association names for different models when using association_join" do + GraphAlbum.association_join(:genres).association_join(:band=>:genres).sql.should == 'SELECT * FROM albums INNER JOIN ag ON (ag.album_id = albums.id) INNER JOIN genres ON (genres.id = ag.genre_id) INNER JOIN bands AS band ON (band.id = albums.band_id) INNER JOIN bg ON (bg.band_id = band.id) INNER JOIN genres AS genres_0 ON (genres_0.id = bg.genre_id)' + end + it "should allow cascading of eager loading for associations of associated models" do ds = GraphTrack.eager_graph(:album=>{:band=>:members}) ds.sql.should == 'SELECT tracks.id, tracks.album_id, album.id AS album_id_0, album.band_id, band.id AS band_id_0, band.vocalist_id, members.id AS members_id FROM tracks LEFT OUTER JOIN albums AS album ON (album.id = tracks.album_id) LEFT OUTER JOIN bands AS band ON (band.id = album.band_id) LEFT OUTER JOIN bm ON (bm.band_id = band.id) LEFT OUTER JOIN members ON (members.id = bm.member_id)'