sequel 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/CHANGELOG +100 -0
  2. data/README.rdoc +3 -3
  3. data/bin/sequel +102 -19
  4. data/doc/reflection.rdoc +83 -0
  5. data/doc/release_notes/3.1.0.txt +406 -0
  6. data/lib/sequel/adapters/ado.rb +11 -0
  7. data/lib/sequel/adapters/amalgalite.rb +5 -20
  8. data/lib/sequel/adapters/do.rb +44 -36
  9. data/lib/sequel/adapters/firebird.rb +29 -43
  10. data/lib/sequel/adapters/jdbc.rb +17 -27
  11. data/lib/sequel/adapters/mysql.rb +35 -40
  12. data/lib/sequel/adapters/odbc.rb +4 -23
  13. data/lib/sequel/adapters/oracle.rb +22 -19
  14. data/lib/sequel/adapters/postgres.rb +6 -15
  15. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  16. data/lib/sequel/adapters/shared/mysql.rb +29 -10
  17. data/lib/sequel/adapters/shared/oracle.rb +6 -8
  18. data/lib/sequel/adapters/shared/postgres.rb +28 -72
  19. data/lib/sequel/adapters/shared/sqlite.rb +5 -3
  20. data/lib/sequel/adapters/sqlite.rb +5 -20
  21. data/lib/sequel/adapters/utils/savepoint_transactions.rb +80 -0
  22. data/lib/sequel/adapters/utils/unsupported.rb +0 -12
  23. data/lib/sequel/core.rb +12 -3
  24. data/lib/sequel/core_sql.rb +1 -8
  25. data/lib/sequel/database.rb +107 -43
  26. data/lib/sequel/database/schema_generator.rb +1 -0
  27. data/lib/sequel/database/schema_methods.rb +38 -4
  28. data/lib/sequel/dataset.rb +6 -0
  29. data/lib/sequel/dataset/convenience.rb +2 -2
  30. data/lib/sequel/dataset/graph.rb +2 -2
  31. data/lib/sequel/dataset/prepared_statements.rb +3 -8
  32. data/lib/sequel/dataset/sql.rb +93 -19
  33. data/lib/sequel/extensions/blank.rb +2 -1
  34. data/lib/sequel/extensions/inflector.rb +4 -3
  35. data/lib/sequel/extensions/migration.rb +13 -2
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pretty_table.rb +4 -0
  38. data/lib/sequel/extensions/query.rb +4 -0
  39. data/lib/sequel/extensions/schema_dumper.rb +100 -24
  40. data/lib/sequel/extensions/string_date_time.rb +3 -4
  41. data/lib/sequel/model.rb +2 -1
  42. data/lib/sequel/model/associations.rb +96 -38
  43. data/lib/sequel/model/base.rb +14 -14
  44. data/lib/sequel/model/plugins.rb +32 -21
  45. data/lib/sequel/plugins/caching.rb +13 -15
  46. data/lib/sequel/plugins/identity_map.rb +107 -0
  47. data/lib/sequel/plugins/lazy_attributes.rb +65 -0
  48. data/lib/sequel/plugins/many_through_many.rb +188 -0
  49. data/lib/sequel/plugins/schema.rb +13 -0
  50. data/lib/sequel/plugins/serialization.rb +53 -37
  51. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  52. data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
  53. data/lib/sequel/plugins/validation_class_methods.rb +28 -7
  54. data/lib/sequel/plugins/validation_helpers.rb +31 -24
  55. data/lib/sequel/sql.rb +16 -0
  56. data/lib/sequel/version.rb +1 -1
  57. data/spec/adapters/ado_spec.rb +47 -1
  58. data/spec/adapters/firebird_spec.rb +39 -36
  59. data/spec/adapters/mysql_spec.rb +25 -9
  60. data/spec/adapters/postgres_spec.rb +11 -24
  61. data/spec/core/database_spec.rb +54 -13
  62. data/spec/core/dataset_spec.rb +147 -29
  63. data/spec/core/object_graph_spec.rb +6 -1
  64. data/spec/core/schema_spec.rb +34 -0
  65. data/spec/core/spec_helper.rb +0 -2
  66. data/spec/extensions/caching_spec.rb +7 -0
  67. data/spec/extensions/identity_map_spec.rb +158 -0
  68. data/spec/extensions/lazy_attributes_spec.rb +113 -0
  69. data/spec/extensions/many_through_many_spec.rb +813 -0
  70. data/spec/extensions/migration_spec.rb +4 -4
  71. data/spec/extensions/schema_dumper_spec.rb +114 -13
  72. data/spec/extensions/schema_spec.rb +19 -3
  73. data/spec/extensions/serialization_spec.rb +28 -0
  74. data/spec/extensions/single_table_inheritance_spec.rb +25 -1
  75. data/spec/extensions/spec_helper.rb +2 -7
  76. data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
  77. data/spec/extensions/validation_class_methods_spec.rb +10 -5
  78. data/spec/integration/dataset_test.rb +39 -6
  79. data/spec/integration/eager_loader_test.rb +7 -7
  80. data/spec/integration/spec_helper.rb +0 -1
  81. data/spec/integration/transaction_test.rb +28 -1
  82. data/spec/model/association_reflection_spec.rb +29 -3
  83. data/spec/model/associations_spec.rb +1 -0
  84. data/spec/model/eager_loading_spec.rb +70 -1
  85. data/spec/model/plugins_spec.rb +236 -50
  86. data/spec/model/spec_helper.rb +0 -2
  87. metadata +18 -5
@@ -23,11 +23,16 @@ describe Sequel::Dataset, " graphing" do
23
23
  ds1.opts.should_not == o1
24
24
  end
25
25
 
26
- it "#graph should accept a dataset as the dataset" do
26
+ it "#graph should accept a simple dataset and pass the table to join" do
27
27
  ds = @ds1.graph(@ds2, :x=>:id)
28
28
  ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
29
29
  end
30
30
 
31
+ it "#graph should accept a complex dataset and pass it directly to join" do
32
+ ds = @ds1.graph(@ds2.filter(:x=>1), {:x=>:id})
33
+ ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points LEFT OUTER JOIN (SELECT * FROM lines WHERE (x = 1)) AS lines ON (lines.x = points.id)'
34
+ end
35
+
31
36
  it "#graph should accept a symbol table name as the dataset" do
32
37
  ds = @ds1.graph(:lines, :x=>:id)
33
38
  ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
@@ -333,6 +333,15 @@ context "DB#create_table" do
333
333
  }.should raise_error(Sequel::Error)
334
334
  end
335
335
 
336
+ specify "should ignore errors if the database raises an error on an index creation statement and the :ignore_index_errors option is used" do
337
+ @db.meta_def(:execute_ddl){|*a| raise Sequel::DatabaseError if /blah/.match(a.first); super(*a)}
338
+ lambda{@db.create_table(:cats){Integer :id; index :blah; index :id}}.should raise_error(Sequel::DatabaseError)
339
+ @db.sqls.should == ['CREATE TABLE cats (id integer)']
340
+ @db.sqls.clear
341
+ lambda{@db.create_table(:cats, :ignore_index_errors=>true){Integer :id; index :blah; index :id}}.should_not raise_error(Sequel::DatabaseError)
342
+ @db.sqls.should == ['CREATE TABLE cats (id integer)', 'CREATE INDEX cats_id_index ON cats (id)']
343
+ end
344
+
336
345
  specify "should accept multiple index definitions" do
337
346
  @db.create_table(:cats) do
338
347
  integer :id
@@ -521,6 +530,24 @@ context "DB#create_table!" do
521
530
  end
522
531
  end
523
532
 
533
+ context "DB#create_table?" do
534
+ before do
535
+ @db = SchemaDummyDatabase.new
536
+ end
537
+
538
+ specify "should not create the table if the table already exists" do
539
+ @db.meta_def(:table_exists?){|a| true}
540
+ @db.create_table?(:cats){|*a|}
541
+ @db.sqls.should == nil
542
+ end
543
+
544
+ specify "should create the table if the table doesn't already exist" do
545
+ @db.meta_def(:table_exists?){|a| false}
546
+ @db.create_table?(:cats){|*a|}
547
+ @db.sqls.should == ['CREATE TABLE cats ()']
548
+ end
549
+ end
550
+
524
551
  context "DB#drop_table" do
525
552
  before do
526
553
  @db = SchemaDummyDatabase.new
@@ -624,6 +651,13 @@ context "DB#alter_table" do
624
651
  @db.sqls.should == ["CREATE INDEX cats_name_index ON cats (name)"]
625
652
  end
626
653
 
654
+ specify "should ignore errors if the database raises an error on an add_index call and the :ignore_errors option is used" do
655
+ @db.meta_def(:execute_ddl){|*a| raise Sequel::DatabaseError}
656
+ lambda{@db.add_index(:cats, :id)}.should raise_error(Sequel::DatabaseError)
657
+ lambda{@db.add_index(:cats, :id, :ignore_errors=>true)}.should_not raise_error(Sequel::DatabaseError)
658
+ @db.sqls.should == nil
659
+ end
660
+
627
661
  specify "should support add_primary_key" do
628
662
  @db.alter_table(:cats) do
629
663
  add_primary_key :id
@@ -4,8 +4,6 @@ unless Object.const_defined?('Sequel')
4
4
  require 'sequel/core'
5
5
  end
6
6
 
7
- Sequel.virtual_row_instance_eval = true
8
-
9
7
  class MockDataset < Sequel::Dataset
10
8
  def insert(*args)
11
9
  @db.execute insert_sql(*args)
@@ -54,6 +54,13 @@ describe Sequel::Model, "caching" do
54
54
  end
55
55
 
56
56
  it "should take a ttl option" do
57
+ c = Class.new(Sequel::Model(:items))
58
+ c.plugin :caching, @cache, :ttl => 1234
59
+ c.cache_ttl.should == 1234
60
+ Class.new(c).cache_ttl.should == 1234
61
+ end
62
+
63
+ it "should allow overriding the ttl option via a plugin :caching call" do
57
64
  @c.plugin :caching, @cache, :ttl => 1234
58
65
  @c.cache_ttl.should == 1234
59
66
  Class.new(@c).cache_ttl.should == 1234
@@ -0,0 +1,158 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "Sequel::Plugins::IdentityMap" do
4
+ before do
5
+ class ::IdentityMapModel < Sequel::Model
6
+ plugin :identity_map
7
+ columns :id
8
+ ds = dataset
9
+ def ds.fetch_rows(sql)
10
+ c = @opts[:where].args.first
11
+ c = c.column if c.is_a?(Sequel::SQL::QualifiedIdentifier)
12
+ h = {c=>@opts[:where].args.last}
13
+ execute(sql)
14
+ yield h
15
+ end
16
+ end
17
+ class ::IdentityMapAlbum < ::IdentityMapModel
18
+ columns :id, :artist_id
19
+ end
20
+ class ::IdentityMapArtist < ::IdentityMapModel
21
+ end
22
+ @c = ::IdentityMapModel
23
+ @c1 = ::IdentityMapAlbum
24
+ @c2 = ::IdentityMapArtist
25
+ MODEL_DB.reset
26
+ end
27
+ after do
28
+ Object.send(:remove_const, :IdentityMapAlbum)
29
+ Object.send(:remove_const, :IdentityMapArtist)
30
+ Object.send(:remove_const, :IdentityMapModel)
31
+ end
32
+
33
+ it "#identity_map should return a hash if an identity map is currently being used" do
34
+ @c.with_identity_map{@c.identity_map.should == {}}
35
+ end
36
+
37
+ it "#identity_map should return nil if an identity map is not currently being used" do
38
+ @c.identity_map.should == nil
39
+ end
40
+
41
+ it "#identity_map_key should be the same for the same class and pk" do
42
+ @c.identity_map_key(1).should == @c.identity_map_key(1)
43
+ end
44
+
45
+ it "#identity_map_key should be different for a different class" do
46
+ @c1.identity_map_key(1).should_not == @c2.identity_map_key(1)
47
+ end
48
+
49
+ it "#identity_map_key should be different for different anonymous classes" do
50
+ Class.new(@c).identity_map_key(1).should_not == Class.new(@c).identity_map_key(1)
51
+ end
52
+
53
+ it "#identity_map_key should be different for a different pk" do
54
+ @c.identity_map_key(1).should_not == @c.identity_map_key(2)
55
+ end
56
+
57
+ it "#identity_map_key should be different if the pk is nil" do
58
+ @c.identity_map_key(nil).should_not == @c.identity_map_key(nil)
59
+ end
60
+
61
+ it "#load should return an object if there is no current identity map" do
62
+ o = @c.load(:id=>1)
63
+ o.should be_a_kind_of(@c)
64
+ o.values.should == {:id=>1}
65
+ end
66
+
67
+ it "#load should return an object if there is a current identity map" do
68
+ @c.with_identity_map do
69
+ o = @c.load(:id=>1)
70
+ o.should be_a_kind_of(@c)
71
+ o.values.should == {:id=>1}
72
+ end
73
+ end
74
+
75
+ it "#load should should store the object in the current identity map if it isn't already there" do
76
+ @c.with_identity_map do
77
+ @c.identity_map[@c.identity_map_key(1)].should == nil
78
+ o = @c.load(:id=>1)
79
+ @c.identity_map[@c.identity_map_key(1)].should == o
80
+ end
81
+ end
82
+
83
+ it "#load should update the record in the current identity map if new fields if it is already there" do
84
+ @c.with_identity_map do
85
+ o = @c.load(:id=>1, :a=>2)
86
+ o.values.should == {:id=>1, :a=>2}
87
+ o = @c.load(:id=>1, :b=>3)
88
+ o.values.should == {:id=>1, :a=>2, :b=>3}
89
+ end
90
+ end
91
+
92
+ it "#load should not update existing fields in the record if the record is in the current identity map" do
93
+ @c.with_identity_map do
94
+ o = @c.load(:id=>1, :a=>2)
95
+ o.values.should == {:id=>1, :a=>2}
96
+ o = @c.load(:id=>1, :a=>4)
97
+ o.values.should == {:id=>1, :a=>2}
98
+ end
99
+ end
100
+
101
+ it "should use the identity map as a lookup cache in Model.[] to save on database queries" do
102
+ @c.with_identity_map do
103
+ MODEL_DB.sqls.length.should == 0
104
+ o = @c[1]
105
+ MODEL_DB.sqls.length.should == 1
106
+ @c[1].should == o
107
+ MODEL_DB.sqls.length.should == 1
108
+ @c[2].should_not == o
109
+ MODEL_DB.sqls.length.should == 2
110
+ end
111
+ end
112
+
113
+ it "should use the identity map as a lookup cache when retrieving many_to_one associated records" do
114
+ @c1.many_to_one :artist, :class=>@c2
115
+ @c.with_identity_map do
116
+ MODEL_DB.sqls.length.should == 0
117
+ o = @c1.load(:id=>1, :artist_id=>2)
118
+ a = o.artist
119
+ a.should be_a_kind_of(@c2)
120
+ MODEL_DB.sqls.length.should == 1
121
+ o = @c1.load(:id=>2, :artist_id=>2)
122
+ o.artist.should == a
123
+ MODEL_DB.sqls.length.should == 1
124
+ o = @c1.load(:id=>3, :artist_id=>3)
125
+ o.artist.should_not == a
126
+ MODEL_DB.sqls.length.should == 2
127
+ end
128
+ end
129
+
130
+ it "should not use the identity map as a lookup cache if the assocation has a nil :key option" do
131
+ c = @c2
132
+ @c1.many_to_one :artist, :class=>@c2, :key=>nil, :dataset=>proc{c.filter(:artist_id=>artist_id)}
133
+ @c.with_identity_map do
134
+ MODEL_DB.sqls.length.should == 0
135
+ o = @c1.load(:id=>1, :artist_id=>2)
136
+ a = o.artist
137
+ a.should be_a_kind_of(@c2)
138
+ MODEL_DB.sqls.length.should == 1
139
+ o = @c1.load(:id=>2, :artist_id=>2)
140
+ o.artist.should == a
141
+ MODEL_DB.sqls.length.should == 2
142
+ end
143
+ end
144
+
145
+ it "should not use the identity map as a lookup cache if the assocation's :primary_key option doesn't match the primary key of the associated class" do
146
+ @c1.many_to_one :artist, :class=>@c2, :primary_key=>:artist_id
147
+ @c.with_identity_map do
148
+ MODEL_DB.sqls.length.should == 0
149
+ o = @c1.load(:id=>1, :artist_id=>2)
150
+ a = o.artist
151
+ a.should be_a_kind_of(@c2)
152
+ MODEL_DB.sqls.length.should == 1
153
+ o = @c1.load(:id=>2, :artist_id=>2)
154
+ o.artist.should == a
155
+ MODEL_DB.sqls.length.should == 2
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,113 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe "Sequel::Plugins::LazyAttributes" do
4
+ before do
5
+ class ::LazyAttributesModel < Sequel::Model(:la)
6
+ plugin :lazy_attributes
7
+ columns :id, :name
8
+ meta_def(:columns){[:id, :name]}
9
+ lazy_attributes :name
10
+ meta_def(:columns){[:id]}
11
+ ds = dataset
12
+ def ds.fetch_rows(sql)
13
+ execute(sql)
14
+ select = @opts[:select]
15
+ where = @opts[:where]
16
+ if !where
17
+ if select.include?(:name)
18
+ yield(:id=>1, :name=>'1')
19
+ yield(:id=>2, :name=>'2')
20
+ else
21
+ yield(:id=>1)
22
+ yield(:id=>2)
23
+ end
24
+ else
25
+ i = where.args.last
26
+ i = i.instance_variable_get(:@array) if i.is_a?(Sequel::SQL::SQLArray)
27
+ Array(i).each do |x|
28
+ if sql =~ /SELECT name FROM/
29
+ yield(:name=>x.to_s)
30
+ else
31
+ yield(:id=>x, :name=>x.to_s)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ @c = ::LazyAttributesModel
38
+ @ds = LazyAttributesModel.dataset
39
+ MODEL_DB.reset
40
+ end
41
+ after do
42
+ Object.send(:remove_const, :LazyAttributesModel)
43
+ end
44
+
45
+ it "should allowing adding additional lazy attributes via plugin :lazy_attributes" do
46
+ @c.set_dataset(@ds.select(:id, :blah))
47
+ @c.dataset.sql.should == 'SELECT id, blah FROM la'
48
+ @c.plugin :lazy_attributes, :blah
49
+ @c.dataset.opts[:select].should == [:id]
50
+ @c.dataset.sql.should == 'SELECT id FROM la'
51
+ end
52
+
53
+ it "should allowing adding additional lazy attributes via lazy_attributes" do
54
+ @c.set_dataset(@ds.select(:id, :blah))
55
+ @c.dataset.sql.should == 'SELECT id, blah FROM la'
56
+ @c.lazy_attributes :blah
57
+ @c.dataset.opts[:select].should == [:id]
58
+ @c.dataset.sql.should == 'SELECT id FROM la'
59
+ end
60
+
61
+ it "should remove the attributes given from the SELECT columns of the model's dataset" do
62
+ @ds.opts[:select].should == [:id]
63
+ @ds.sql.should == 'SELECT id FROM la'
64
+ end
65
+
66
+ it "should lazily load the attribute for a single model object if there is an active identity map" do
67
+ @c.with_identity_map do
68
+ m = @c.first
69
+ m.values.should == {:id=>1}
70
+ m.name.should == '1'
71
+ m.values.should == {:id=>1, :name=>'1'}
72
+ MODEL_DB.sqls.should == ['SELECT id FROM la LIMIT 1', 'SELECT name FROM la WHERE (id = 1) LIMIT 1']
73
+ end
74
+ end
75
+
76
+ it "should lazily load the attribute for a single model object if there is no active identity map" do
77
+ m = @c.first
78
+ m.values.should == {:id=>1}
79
+ m.name.should == '1'
80
+ m.values.should == {:id=>1, :name=>'1'}
81
+ MODEL_DB.sqls.should == ['SELECT id FROM la LIMIT 1', 'SELECT name FROM la WHERE (id = 1) LIMIT 1']
82
+ end
83
+
84
+ it "should not lazily load the attribute for a single model object if the value already exists" do
85
+ @c.with_identity_map do
86
+ m = @c.first
87
+ m.values.should == {:id=>1}
88
+ m[:name] = '1'
89
+ m.name.should == '1'
90
+ m.values.should == {:id=>1, :name=>'1'}
91
+ MODEL_DB.sqls.should == ['SELECT id FROM la LIMIT 1']
92
+ end
93
+ end
94
+
95
+ it "should not lazily load the attribute for a single model object if it is a new record" do
96
+ @c.with_identity_map do
97
+ m = @c.new
98
+ m.values.should == {}
99
+ m.name.should == nil
100
+ MODEL_DB.sqls.should == []
101
+ end
102
+ end
103
+
104
+ it "should eagerly load the attribute for all model objects reteived with it" do
105
+ @c.with_identity_map do
106
+ ms = @c.all
107
+ ms.map{|m| m.values}.should == [{:id=>1}, {:id=>2}]
108
+ ms.map{|m| m.name}.should == %w'1 2'
109
+ ms.map{|m| m.values}.should == [{:id=>1, :name=>'1'}, {:id=>2, :name=>'2'}]
110
+ MODEL_DB.sqls.should == ['SELECT id FROM la', 'SELECT id, name FROM la WHERE (id IN (1, 2))']
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,813 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe Sequel::Model, "many_through_many" do
4
+ before do
5
+ class ::Artist < Sequel::Model
6
+ attr_accessor :yyy
7
+ columns :id
8
+ plugin :many_through_many
9
+ end
10
+ class ::Tag < Sequel::Model
11
+ end
12
+ MODEL_DB.reset
13
+ @c1 = Artist
14
+ @c2 = Tag
15
+ @dataset = @c2.dataset
16
+ def @dataset.fetch_rows(sql)
17
+ @db << sql
18
+ yield({:id=>1})
19
+ end
20
+ end
21
+ after do
22
+ Object.send(:remove_const, :Artist)
23
+ Object.send(:remove_const, :Tag)
24
+ end
25
+
26
+ it "should raise an error if in invalid form of through is used" do
27
+ proc{@c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id]]}.should raise_error(Sequel::Error)
28
+ proc{@c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], {:table=>:album_tags, :left=>:album_id}]}.should raise_error(Sequel::Error)
29
+ proc{@c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], :album_tags]}.should raise_error(Sequel::Error)
30
+ end
31
+
32
+ it "should use join tables given" do
33
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
34
+ n = @c1.load(:id => 1234)
35
+ a = n.tags_dataset
36
+ a.should be_a_kind_of(Sequel::Dataset)
37
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))'
38
+ n.tags.should == [@c2.load(:id=>1)]
39
+ end
40
+
41
+ it "should handle multiple aliasing of tables" do
42
+ class ::Album < Sequel::Model
43
+ end
44
+ @c1.many_through_many :albums, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_artists, :album_id, :artist_id], [:artists, :id, :id], [:albums_artists, :artist_id, :album_id]]
45
+ n = @c1.load(:id => 1234)
46
+ a = n.albums_dataset
47
+ a.should be_a_kind_of(Sequel::Dataset)
48
+ a.sql.should == 'SELECT albums.* FROM albums INNER JOIN albums_artists ON (albums_artists.album_id = albums.id) INNER JOIN artists ON (artists.id = albums_artists.artist_id) INNER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) INNER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id) INNER JOIN albums_artists AS albums_artists_1 ON ((albums_artists_1.album_id = albums_0.id) AND (albums_artists_1.artist_id = 1234))'
49
+ n.albums.should == [Album.load(:id=>1, :x=>1)]
50
+ Object.send(:remove_const, :Album)
51
+ end
52
+
53
+ it "should use explicit class if given" do
54
+ @c1.many_through_many :albums_tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :class=>Tag
55
+ n = @c1.load(:id => 1234)
56
+ a = n.albums_tags_dataset
57
+ a.should be_a_kind_of(Sequel::Dataset)
58
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))'
59
+ n.albums_tags.should == [@c2.load(:id=>1)]
60
+ end
61
+
62
+ it "should accept :left_primary_key and :right_primary_key option for primary keys to use in current and associated table" do
63
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :right_primary_key=>:tag_id, :left_primary_key=>:yyy
64
+ n = @c1.load(:id => 1234)
65
+ n.yyy = 85
66
+ a = n.tags_dataset
67
+ a.should be_a_kind_of(Sequel::Dataset)
68
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.tag_id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 85))'
69
+ n.tags.should == [@c2.load(:id=>1)]
70
+ end
71
+
72
+ it "should support a :conditions option" do
73
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :conditions=>{:a=>32}
74
+ n = @c1.load(:id => 1234)
75
+ a = n.tags_dataset
76
+ a.should be_a_kind_of(Sequel::Dataset)
77
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) WHERE (a = 32)'
78
+ n.tags.should == [@c2.load(:id=>1)]
79
+
80
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :conditions=>['a = ?', 42]
81
+ n = @c1.load(:id => 1234)
82
+ a = n.tags_dataset
83
+ a.should be_a_kind_of(Sequel::Dataset)
84
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) WHERE (a = 42)'
85
+ n.tags.should == [@c2.load(:id=>1)]
86
+ end
87
+
88
+ it "should support an :order option" do
89
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :order=>:blah
90
+ n = @c1.load(:id => 1234)
91
+ a = n.tags_dataset
92
+ a.should be_a_kind_of(Sequel::Dataset)
93
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) ORDER BY blah'
94
+ n.tags.should == [@c2.load(:id=>1)]
95
+ end
96
+
97
+ it "should support an array for the :order option" do
98
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :order=>[:blah1, :blah2]
99
+ n = @c1.load(:id => 1234)
100
+ a = n.tags_dataset
101
+ a.should be_a_kind_of(Sequel::Dataset)
102
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) ORDER BY blah1, blah2'
103
+ n.tags.should == [@c2.load(:id=>1)]
104
+ end
105
+
106
+ it "should support a select option" do
107
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :select=>:blah
108
+ n = @c1.load(:id => 1234)
109
+ a = n.tags_dataset
110
+ a.should be_a_kind_of(Sequel::Dataset)
111
+ a.sql.should == 'SELECT blah FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))'
112
+ n.tags.should == [@c2.load(:id=>1)]
113
+ end
114
+
115
+ it "should support an array for the select option" do
116
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :select=>[:tags.*, :albums__name]
117
+ n = @c1.load(:id => 1234)
118
+ a = n.tags_dataset
119
+ a.should be_a_kind_of(Sequel::Dataset)
120
+ a.sql.should == 'SELECT tags.*, albums.name FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))'
121
+ n.tags.should == [@c2.load(:id=>1)]
122
+ end
123
+
124
+ it "should accept a block" do
125
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]] do |ds| ds.filter(:yyy=>@yyy) end
126
+ n = @c1.load(:id => 1234)
127
+ n.yyy = 85
128
+ a = n.tags_dataset
129
+ a.should be_a_kind_of(Sequel::Dataset)
130
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) WHERE (yyy = 85)'
131
+ n.tags.should == [@c2.load(:id=>1)]
132
+ end
133
+
134
+ it "should allow the :order option while accepting a block" do
135
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :order=>:blah do |ds| ds.filter(:yyy=>@yyy) end
136
+ n = @c1.load(:id => 1234)
137
+ n.yyy = 85
138
+ a = n.tags_dataset
139
+ a.should be_a_kind_of(Sequel::Dataset)
140
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) WHERE (yyy = 85) ORDER BY blah'
141
+ n.tags.should == [@c2.load(:id=>1)]
142
+ end
143
+
144
+ it "should support a :dataset option that is used instead of the default" do
145
+ @c1.many_through_many :tags, [[:a, :b, :c]], :dataset=>proc{Tag.join(:albums_tags, [:tag_id]).join(:albums, [:album_id]).join(:albums_artists, [:album_id]).filter(:albums_artists__artist_id=>id)}
146
+ n = @c1.load(:id => 1234)
147
+ a = n.tags_dataset
148
+ a.should be_a_kind_of(Sequel::Dataset)
149
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags USING (tag_id) INNER JOIN albums USING (album_id) INNER JOIN albums_artists USING (album_id) WHERE (albums_artists.artist_id = 1234)'
150
+ n.tags.should == [@c2.load(:id=>1)]
151
+ end
152
+
153
+ it "should support a :limit option" do
154
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :limit=>10
155
+ n = @c1.load(:id => 1234)
156
+ a = n.tags_dataset
157
+ a.should be_a_kind_of(Sequel::Dataset)
158
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) LIMIT 10'
159
+ n.tags.should == [@c2.load(:id=>1)]
160
+
161
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :limit=>[10, 10]
162
+ n = @c1.load(:id => 1234)
163
+ a = n.tags_dataset
164
+ a.should be_a_kind_of(Sequel::Dataset)
165
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234)) LIMIT 10 OFFSET 10'
166
+ n.tags.should == [@c2.load(:id=>1)]
167
+ end
168
+
169
+ it "should have the :eager option affect the _dataset method" do
170
+ @c2.many_to_many :fans
171
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :eager=>:fans
172
+ @c1.load(:id => 1234).tags_dataset.opts[:eager].should == {:fans=>nil}
173
+ end
174
+
175
+ it "should provide an array with all members of the association" do
176
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
177
+ @c1.load(:id => 1234).tags.should == [@c2.load(:id=>1)]
178
+ MODEL_DB.sqls.should == ['SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))']
179
+ end
180
+
181
+ it "should set cached instance variable when accessed" do
182
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
183
+ n = @c1.load(:id => 1234)
184
+ n.associations[:tags].should == nil
185
+ MODEL_DB.sqls.should == []
186
+ n.tags.should == [@c2.load(:id=>1)]
187
+ MODEL_DB.sqls.should == ['SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))']
188
+ n.associations[:tags].should == n.tags
189
+ MODEL_DB.sqls.length.should == 1
190
+ end
191
+
192
+ it "should use cached instance variable if available" do
193
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
194
+ n = @c1.load(:id => 1234)
195
+ n.associations[:tags] = []
196
+ n.tags.should == []
197
+ MODEL_DB.sqls.should == []
198
+ end
199
+
200
+ it "should not use cached instance variable if asked to reload" do
201
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
202
+ n = @c1.load(:id => 1234)
203
+ n.associations[:tags] = []
204
+ MODEL_DB.sqls.should == []
205
+ n.tags(true).should == [@c2.load(:id=>1)]
206
+ MODEL_DB.sqls.should == ['SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))']
207
+ n.associations[:tags].should == n.tags
208
+ MODEL_DB.sqls.length.should == 1
209
+ end
210
+
211
+ it "should not add associations methods directly to class" do
212
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
213
+ im = @c1.instance_methods.collect{|x| x.to_s}
214
+ im.should(include('tags'))
215
+ im.should(include('tags_dataset'))
216
+ im2 = @c1.instance_methods(false).collect{|x| x.to_s}
217
+ im2.should_not(include('tags'))
218
+ im2.should_not(include('tags_dataset'))
219
+ end
220
+
221
+ it "should support after_load association callback" do
222
+ h = []
223
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :after_load=>:al
224
+ @c1.class_eval do
225
+ @@blah = h
226
+ def al(v)
227
+ v.each{|x| @@blah << x.pk * 20}
228
+ end
229
+ end
230
+ @c2.class_eval do
231
+ def @dataset.fetch_rows(sql)
232
+ yield({:id=>20})
233
+ yield({:id=>30})
234
+ end
235
+ end
236
+ p = @c1.load(:id=>10, :parent_id=>20)
237
+ p.tags
238
+ h.should == [400, 600]
239
+ p.tags.collect{|a| a.pk}.should == [20, 30]
240
+ end
241
+
242
+ it "should support a :uniq option that removes duplicates from the association" do
243
+ h = []
244
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :uniq=>true
245
+ @c2.class_eval do
246
+ def @dataset.fetch_rows(sql)
247
+ yield({:id=>20})
248
+ yield({:id=>30})
249
+ yield({:id=>20})
250
+ yield({:id=>30})
251
+ end
252
+ end
253
+ @c1.load(:id=>10).tags.should == [@c2.load(:id=>20), @c2.load(:id=>30)]
254
+ end
255
+ end
256
+
257
+ describe 'Sequel::Plugins::ManyThroughMany::ManyThroughManyAssociationReflection' do
258
+ before do
259
+ class ::Artist < Sequel::Model
260
+ plugin :many_through_many
261
+ many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
262
+ end
263
+ class ::Tag < Sequel::Model
264
+ end
265
+ MODEL_DB.reset
266
+ @ar = Artist.association_reflection(:tags)
267
+ end
268
+ after do
269
+ Object.send(:remove_const, :Artist)
270
+ Object.send(:remove_const, :Tag)
271
+ end
272
+
273
+ it "#edges should be an array of joins to make when eager graphing" do
274
+ @ar.edges.should == [{:conditions=>[], :left=>:id, :right=>:artist_id, :table=>:albums_artists, :join_type=>:left_outer, :block=>nil}, {:conditions=>[], :left=>:album_id, :right=>:id, :table=>:albums, :join_type=>:left_outer, :block=>nil}, {:conditions=>[], :left=>:id, :right=>:album_id, :table=>:albums_tags, :join_type=>:left_outer, :block=>nil}]
275
+ end
276
+
277
+ it "#reverse_edges should be an array of joins to make when lazy loading or eager loading" do
278
+ @ar.reverse_edges.should == [{:alias=>:albums_tags, :left=>:tag_id, :right=>:id, :table=>:albums_tags}, {:alias=>:albums, :left=>:id, :right=>:album_id, :table=>:albums}]
279
+ end
280
+
281
+ it "#reciprocal should be nil" do
282
+ @ar.reciprocal.should == nil
283
+ end
284
+ end
285
+
286
+ describe "Sequel::Plugins::ManyThroughMany eager loading methods" do
287
+ before do
288
+ class ::Artist < Sequel::Model
289
+ plugin :many_through_many
290
+ many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
291
+ many_through_many :other_tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :class=>:Tag
292
+ many_through_many :albums, [[:albums_artists, :artist_id, :album_id]]
293
+ many_through_many :artists, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_artists, :album_id, :artist_id]]
294
+ end
295
+ class ::Tag < Sequel::Model
296
+ plugin :many_through_many
297
+ many_through_many :tracks, [[:albums_tags, :tag_id, :album_id], [:albums, :id, :id]], :right_primary_key=>:album_id
298
+ end
299
+ class ::Album < Sequel::Model
300
+ end
301
+ class ::Track < Sequel::Model
302
+ end
303
+
304
+ Artist.dataset.extend(Module.new {
305
+ def columns
306
+ [:id]
307
+ end
308
+
309
+ def fetch_rows(sql)
310
+ @db << sql
311
+ h = {:id => 1}
312
+ if sql =~ /FROM artists LEFT OUTER JOIN albums_artists/
313
+ h[:tags_id] = 2
314
+ h[:albums_0_id] = 3 if sql =~ /LEFT OUTER JOIN albums AS albums_0/
315
+ h[:tracks_id] = 4 if sql =~ /LEFT OUTER JOIN tracks/
316
+ h[:other_tags_id] = 9 if sql =~ /other_tags\.id AS other_tags_id/
317
+ h[:artists_0_id] = 10 if sql =~ /artists_0\.id AS artists_0_id/
318
+ end
319
+ yield h
320
+ end
321
+ })
322
+
323
+ Tag.dataset.extend(Module.new {
324
+ def fetch_rows(sql)
325
+ @db << sql
326
+ h = {:id => 2}
327
+ if sql =~ /albums_artists.artist_id IN \(([18])\)/
328
+ h.merge!(:x_foreign_key_x=>$1.to_i)
329
+ end
330
+ h[:tag_id] = h.delete(:id) if sql =~ /albums_artists.artist_id IN \(8\)/
331
+ yield h
332
+ end
333
+ })
334
+
335
+ Album.dataset.extend(Module.new {
336
+ def fetch_rows(sql)
337
+ @db << sql
338
+ h = {:id => 3}
339
+ h.merge!(:x_foreign_key_x=>1) if sql =~ /albums_artists.artist_id IN \(1\)/
340
+ yield h
341
+ end
342
+ })
343
+
344
+ Track.dataset.extend(Module.new {
345
+ def fetch_rows(sql)
346
+ @db << sql
347
+ h = {:id => 4}
348
+ h.merge!(:x_foreign_key_x=>2) if sql =~ /albums_tags.tag_id IN \(2\)/
349
+ yield h
350
+ end
351
+ })
352
+
353
+ @c1 = Artist
354
+ MODEL_DB.reset
355
+ end
356
+ after do
357
+ [:Artist, :Tag, :Album, :Track].each{|x| Object.send(:remove_const, x)}
358
+ end
359
+
360
+ it "should eagerly load a single many_through_many association" do
361
+ a = @c1.eager(:tags).all
362
+ a.should == [@c1.load(:id=>1)]
363
+ MODEL_DB.sqls.should == ['SELECT * FROM artists', 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))']
364
+ a.first.tags.should == [Tag.load(:id=>2)]
365
+ MODEL_DB.sqls.length.should == 2
366
+ end
367
+
368
+ it "should eagerly load multiple associations in a single call" do
369
+ a = @c1.eager(:tags, :albums).all
370
+ a.should == [@c1.load(:id=>1)]
371
+ MODEL_DB.sqls.length.should == 3
372
+ MODEL_DB.sqls[0].should == 'SELECT * FROM artists'
373
+ MODEL_DB.sqls[1..-1].should(include('SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))'))
374
+ MODEL_DB.sqls[1..-1].should(include('SELECT albums.*, albums_artists.artist_id AS x_foreign_key_x FROM albums INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))'))
375
+ a = a.first
376
+ a.tags.should == [Tag.load(:id=>2)]
377
+ a.albums.should == [Album.load(:id=>3)]
378
+ MODEL_DB.sqls.length.should == 3
379
+ end
380
+
381
+ it "should eagerly load multiple associations in separate" do
382
+ a = @c1.eager(:tags).eager(:albums).all
383
+ a.should == [@c1.load(:id=>1)]
384
+ MODEL_DB.sqls.length.should == 3
385
+ MODEL_DB.sqls[0].should == 'SELECT * FROM artists'
386
+ MODEL_DB.sqls[1..-1].should(include('SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))'))
387
+ MODEL_DB.sqls[1..-1].should(include('SELECT albums.*, albums_artists.artist_id AS x_foreign_key_x FROM albums INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))'))
388
+ a = a.first
389
+ a.tags.should == [Tag.load(:id=>2)]
390
+ a.albums.should == [Album.load(:id=>3)]
391
+ MODEL_DB.sqls.length.should == 3
392
+ end
393
+
394
+ it "should allow cascading of eager loading for associations of associated models" do
395
+ a = @c1.eager(:tags=>:tracks).all
396
+ a.should == [@c1.load(:id=>1)]
397
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
398
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))',
399
+ 'SELECT tracks.*, albums_tags.tag_id AS x_foreign_key_x FROM tracks INNER JOIN albums ON (albums.id = tracks.album_id) INNER JOIN albums_tags ON ((albums_tags.album_id = albums.id) AND (albums_tags.tag_id IN (2)))']
400
+ a = a.first
401
+ a.tags.should == [Tag.load(:id=>2)]
402
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
403
+ MODEL_DB.sqls.length.should == 3
404
+ end
405
+
406
+ it "should cascade eagerly loading when the :eager association option is used" do
407
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :eager=>:tracks
408
+ a = @c1.eager(:tags).all
409
+ a.should == [@c1.load(:id=>1)]
410
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
411
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))',
412
+ 'SELECT tracks.*, albums_tags.tag_id AS x_foreign_key_x FROM tracks INNER JOIN albums ON (albums.id = tracks.album_id) INNER JOIN albums_tags ON ((albums_tags.album_id = albums.id) AND (albums_tags.tag_id IN (2)))']
413
+ a = a.first
414
+ a.tags.should == [Tag.load(:id=>2)]
415
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
416
+ MODEL_DB.sqls.length.should == 3
417
+ end
418
+
419
+ it "should respect :eager when lazily loading an association" do
420
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :eager=>:tracks
421
+ a = @c1.load(:id=>1)
422
+ a.tags.should == [Tag.load(:id=>2)]
423
+ MODEL_DB.sqls.should == ['SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1))',
424
+ 'SELECT tracks.*, albums_tags.tag_id AS x_foreign_key_x FROM tracks INNER JOIN albums ON (albums.id = tracks.album_id) INNER JOIN albums_tags ON ((albums_tags.album_id = albums.id) AND (albums_tags.tag_id IN (2)))']
425
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
426
+ MODEL_DB.sqls.length.should == 2
427
+ end
428
+
429
+ it "should cascade eagerly loading when the :eager_graph association option is used" do
430
+ Tag.dataset.extend(Module.new {
431
+ def columns
432
+ [:id]
433
+ end
434
+ def fetch_rows(sql)
435
+ @db << sql
436
+ yield({:id=>2, :tracks_id=>4, :x_foreign_key_x=>1})
437
+ end
438
+ })
439
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :eager_graph=>:tracks
440
+ a = @c1.eager(:tags).all
441
+ a.should == [@c1.load(:id=>1)]
442
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
443
+ 'SELECT tags.id, tracks.id AS tracks_id, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1))) LEFT OUTER JOIN albums_tags AS albums_tags_0 ON (albums_tags_0.tag_id = tags.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_tags_0.album_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums_0.id)']
444
+ a = a.first
445
+ a.tags.should == [Tag.load(:id=>2)]
446
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
447
+ MODEL_DB.sqls.length.should == 2
448
+ end
449
+
450
+ it "should respect :eager_graph when lazily loading an association" do
451
+ Tag.dataset.extend(Module.new {
452
+ def columns
453
+ [:id]
454
+ end
455
+ def fetch_rows(sql)
456
+ @db << sql
457
+ yield({:id=>2, :tracks_id=>4})
458
+ end
459
+ })
460
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :eager_graph=>:tracks
461
+ a = @c1.load(:id=>1)
462
+ a.tags
463
+ MODEL_DB.sqls.should == [ 'SELECT tags.id, tracks.id AS tracks_id FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1)) LEFT OUTER JOIN albums_tags AS albums_tags_0 ON (albums_tags_0.tag_id = tags.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_tags_0.album_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums_0.id)']
464
+ a.tags.should == [Tag.load(:id=>2)]
465
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
466
+ MODEL_DB.sqls.length.should == 1
467
+ end
468
+
469
+ it "should respect :conditions when eagerly loading" do
470
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :conditions=>{:a=>32}
471
+ a = @c1.eager(:tags).all
472
+ a.should == [@c1.load(:id=>1)]
473
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
474
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1))) WHERE (a = 32)']
475
+ a.first.tags.should == [Tag.load(:id=>2)]
476
+ MODEL_DB.sqls.length.should == 2
477
+ end
478
+
479
+ it "should respect :order when eagerly loading" do
480
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :order=>:blah
481
+ a = @c1.eager(:tags).all
482
+ a.should == [@c1.load(:id=>1)]
483
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
484
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1))) ORDER BY blah']
485
+ a.first.tags.should == [Tag.load(:id=>2)]
486
+ MODEL_DB.sqls.length.should == 2
487
+ end
488
+
489
+ it "should use the association's block when eager loading by default" do
490
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]] do |ds| ds.filter(:a) end
491
+ a = @c1.eager(:tags).all
492
+ a.should == [@c1.load(:id=>1)]
493
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
494
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1))) WHERE a']
495
+ a.first.tags.should == [Tag.load(:id=>2)]
496
+ MODEL_DB.sqls.length.should == 2
497
+ end
498
+
499
+ it "should use the :eager_block option when eager loading if given" do
500
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :eager_block=>proc{|ds| ds.filter(:b)} do |ds| ds.filter(:a) end
501
+ a = @c1.eager(:tags).all
502
+ a.should == [@c1.load(:id=>1)]
503
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
504
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1))) WHERE b']
505
+ a.first.tags.should == [Tag.load(:id=>2)]
506
+ MODEL_DB.sqls.length.should == 2
507
+ end
508
+
509
+ it "should raise an error when attempting to eagerly load an association with the :allow_eager option set to false" do
510
+ proc{@c1.eager(:tags).all}.should_not raise_error
511
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :allow_eager=>false
512
+ proc{@c1.eager(:tags).all}.should raise_error(Sequel::Error)
513
+ end
514
+
515
+ it "should respect the association's :select option" do
516
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :select=>:tags__name
517
+ a = @c1.eager(:tags).all
518
+ a.should == [@c1.load(:id=>1)]
519
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
520
+ 'SELECT tags.name, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))']
521
+ a.first.tags.should == [Tag.load(:id=>2)]
522
+ MODEL_DB.sqls.length.should == 2
523
+ end
524
+
525
+ it "should respect many_to_many association's :left_primary_key and :right_primary_key options" do
526
+ @c1.send(:define_method, :yyy){values[:yyy]}
527
+ @c1.dataset.extend(Module.new {
528
+ def columns
529
+ [:id, :yyy]
530
+ end
531
+ def fetch_rows(sql)
532
+ @db << sql
533
+ yield({:id=>1, :yyy=>8})
534
+ end
535
+ })
536
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :left_primary_key=>:yyy, :right_primary_key=>:tag_id
537
+ a = @c1.eager(:tags).all
538
+ a.should == [@c1.load(:id=>1, :yyy=>8)]
539
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
540
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.tag_id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (8)))']
541
+ a.first.tags.should == [Tag.load(:tag_id=>2)]
542
+ MODEL_DB.sqls.length.should == 2
543
+ end
544
+
545
+ it "should respect :after_load callbacks on associations when eager loading" do
546
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :after_load=>lambda{|o, as| o[:id] *= 2; as.each{|a| a[:id] *= 3}}
547
+ a = @c1.eager(:tags).all
548
+ a.should == [@c1.load(:id=>2)]
549
+ MODEL_DB.sqls.should == ['SELECT * FROM artists',
550
+ 'SELECT tags.*, albums_artists.artist_id AS x_foreign_key_x FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))']
551
+ a.first.tags.should == [Tag.load(:id=>6)]
552
+ MODEL_DB.sqls.length.should == 2
553
+ end
554
+
555
+ it "should raise an error if called without a symbol or hash" do
556
+ proc{@c1.eager_graph(Object.new)}.should raise_error(Sequel::Error)
557
+ end
558
+
559
+ it "should eagerly graph a single many_through_many association" do
560
+ a = @c1.eager_graph(:tags).all
561
+ a.should == [@c1.load(:id=>1)]
562
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)']
563
+ a.first.tags.should == [Tag.load(:id=>2)]
564
+ MODEL_DB.sqls.length.should == 1
565
+ end
566
+
567
+ it "should eagerly graph multiple associations in a single call" do
568
+ a = @c1.eager_graph(:tags, :albums).all
569
+ a.should == [@c1.load(:id=>1)]
570
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, albums_0.id AS albums_0_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id)']
571
+ a = a.first
572
+ a.tags.should == [Tag.load(:id=>2)]
573
+ a.albums.should == [Album.load(:id=>3)]
574
+ MODEL_DB.sqls.length.should == 1
575
+ end
576
+
577
+ it "should eagerly graph multiple associations in separate calls" do
578
+ a = @c1.eager_graph(:tags).eager_graph(:albums).all
579
+ a.should == [@c1.load(:id=>1)]
580
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, albums_0.id AS albums_0_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id)']
581
+ a = a.first
582
+ a.tags.should == [Tag.load(:id=>2)]
583
+ a.albums.should == [Album.load(:id=>3)]
584
+ MODEL_DB.sqls.length.should == 1
585
+ end
586
+
587
+ it "should allow cascading of eager graphing for associations of associated models" do
588
+ a = @c1.eager_graph(:tags=>:tracks).all
589
+ a.should == [@c1.load(:id=>1)]
590
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, tracks.id AS tracks_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_tags AS albums_tags_0 ON (albums_tags_0.tag_id = tags.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_tags_0.album_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums_0.id)']
591
+ a = a.first
592
+ a.tags.should == [Tag.load(:id=>2)]
593
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
594
+ MODEL_DB.sqls.length.should == 1
595
+ end
596
+
597
+ it "eager graphing should eliminate duplicates caused by cartesian products" do
598
+ ds = @c1.eager_graph(:tags)
599
+ def ds.fetch_rows(sql, &block)
600
+ @db << sql
601
+ # Assume artist has 2 albums each with 2 tags
602
+ yield({:id=>1, :tags_id=>2})
603
+ yield({:id=>1, :tags_id=>3})
604
+ yield({:id=>1, :tags_id=>2})
605
+ yield({:id=>1, :tags_id=>3})
606
+ end
607
+ a = ds.all
608
+ a.should == [@c1.load(:id=>1)]
609
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)']
610
+ a.first.tags.should == [Tag.load(:id=>2), Tag.load(:id=>3)]
611
+ MODEL_DB.sqls.length.should == 1
612
+ end
613
+
614
+ it "should eager graph multiple associations from the same table" do
615
+ a = @c1.eager_graph(:tags, :other_tags).all
616
+ a.should == [@c1.load(:id=>1)]
617
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, other_tags.id AS other_tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id) LEFT OUTER JOIN albums_tags AS albums_tags_0 ON (albums_tags_0.album_id = albums_0.id) LEFT OUTER JOIN tags AS other_tags ON (other_tags.id = albums_tags_0.tag_id)']
618
+ a = a.first
619
+ a.tags.should == [Tag.load(:id=>2)]
620
+ a.other_tags.should == [Tag.load(:id=>9)]
621
+ MODEL_DB.sqls.length.should == 1
622
+ end
623
+
624
+ it "should eager graph a self_referential association" do
625
+ a = @c1.eager_graph(:tags, :artists).all
626
+ a.should == [@c1.load(:id=>1)]
627
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, artists_0.id AS artists_0_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id) LEFT OUTER JOIN albums_artists AS albums_artists_1 ON (albums_artists_1.album_id = albums_0.id) LEFT OUTER JOIN artists AS artists_0 ON (artists_0.id = albums_artists_1.artist_id)']
628
+ a = a.first
629
+ a.tags.should == [Tag.load(:id=>2)]
630
+ a.artists.should == [@c1.load(:id=>10)]
631
+ MODEL_DB.sqls.length.should == 1
632
+ end
633
+
634
+ it "eager graphing should give you a graph of tables when called without .all" do
635
+ @c1.eager_graph(:tags, :artists).first.should == {:artists=>@c1.load(:id=>1), :artists_0=>@c1.load(:id=>10), :tags=>Tag.load(:id=>2)}
636
+ end
637
+
638
+ it "should be able to use eager and eager_graph together" do
639
+ a = @c1.eager_graph(:tags).eager(:albums).all
640
+ a.should == [@c1.load(:id=>1)]
641
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)',
642
+ 'SELECT albums.*, albums_artists.artist_id AS x_foreign_key_x FROM albums INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id IN (1)))']
643
+ a = a.first
644
+ a.tags.should == [Tag.load(:id=>2)]
645
+ a.albums.should == [Album.load(:id=>3)]
646
+ MODEL_DB.sqls.length.should == 2
647
+ end
648
+
649
+ it "should handle no associated records when eagerly graphing a single many_through_many association" do
650
+ ds = @c1.eager_graph(:tags)
651
+ def ds.fetch_rows(sql)
652
+ @db << sql
653
+ yield({:id=>1, :tags_id=>nil})
654
+ end
655
+ a = ds.all
656
+ a.should == [@c1.load(:id=>1)]
657
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)']
658
+ a.first.tags.should == []
659
+ MODEL_DB.sqls.length.should == 1
660
+ end
661
+
662
+ it "should handle no associated records when eagerly graphing multiple many_through_many associations" do
663
+ ds = @c1.eager_graph(:tags, :albums)
664
+ def ds.fetch_rows(sql)
665
+ @db << sql
666
+ yield({:id=>1, :tags_id=>nil, :albums_0_id=>3})
667
+ yield({:id=>1, :tags_id=>2, :albums_0_id=>nil})
668
+ yield({:id=>1, :tags_id=>5, :albums_0_id=>6})
669
+ yield({:id=>7, :tags_id=>nil, :albums_0_id=>nil})
670
+ end
671
+ a = ds.all
672
+ a.should == [@c1.load(:id=>1), @c1.load(:id=>7)]
673
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, albums_0.id AS albums_0_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id)']
674
+ a.first.tags.should == [Tag.load(:id=>2), Tag.load(:id=>5)]
675
+ a.first.albums.should == [Album.load(:id=>3), Album.load(:id=>6)]
676
+ a.last.tags.should == []
677
+ a.last.albums.should == []
678
+ MODEL_DB.sqls.length.should == 1
679
+ end
680
+
681
+ it "should handle missing associated records when cascading eager graphing for associations of associated models" do
682
+ ds = @c1.eager_graph(:tags=>:tracks)
683
+ def ds.fetch_rows(sql)
684
+ @db << sql
685
+ yield({:id=>1, :tags_id=>2, :tracks_id=>4})
686
+ yield({:id=>1, :tags_id=>3, :tracks_id=>nil})
687
+ yield({:id=>2, :tags_id=>nil, :tracks_id=>nil})
688
+ end
689
+ a = ds.all
690
+ a.should == [@c1.load(:id=>1), @c1.load(:id=>2)]
691
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.id AS tags_id, tracks.id AS tracks_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_tags AS albums_tags_0 ON (albums_tags_0.tag_id = tags.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_tags_0.album_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums_0.id)']
692
+ a.last.tags.should == []
693
+ a = a.first
694
+ a.tags.should == [Tag.load(:id=>2), Tag.load(:id=>3)]
695
+ a.tags.first.tracks.should == [Track.load(:id=>4)]
696
+ a.tags.last.tracks.should == []
697
+ MODEL_DB.sqls.length.should == 1
698
+ end
699
+
700
+ it "eager graphing should respect :left_primary_key and :right_primary_key options" do
701
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :left_primary_key=>:yyy, :right_primary_key=>:tag_id
702
+ @c1.dataset.meta_def(:columns){[:id, :yyy]}
703
+ Tag.dataset.meta_def(:columns){[:id, :tag_id]}
704
+ ds = @c1.eager_graph(:tags)
705
+ def ds.fetch_rows(sql)
706
+ @db << sql
707
+ yield({:id=>1, :yyy=>8, :tags_id=>2, :tag_id=>4})
708
+ end
709
+ a = ds.all
710
+ a.should == [@c1.load(:id=>1, :yyy=>8)]
711
+ MODEL_DB.sqls.should == ['SELECT artists.id, artists.yyy, tags.id AS tags_id, tags.tag_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.yyy) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.tag_id = albums_tags.tag_id)']
712
+ a.first.tags.should == [Tag.load(:id=>2, :tag_id=>4)]
713
+ MODEL_DB.sqls.length.should == 1
714
+ end
715
+
716
+ it "should respect the association's :graph_select option" do
717
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :graph_select=>:b
718
+ ds = @c1.eager_graph(:tags)
719
+ def ds.fetch_rows(sql)
720
+ @db << sql
721
+ yield({:id=>1, :b=>2})
722
+ end
723
+ a = ds.all
724
+ a.should == [@c1.load(:id=>1)]
725
+ MODEL_DB.sqls.should == ['SELECT artists.id, tags.b FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)']
726
+ a.first.tags.should == [Tag.load(:b=>2)]
727
+ MODEL_DB.sqls.length.should == 1
728
+ end
729
+
730
+ it "should respect the association's :graph_join_type option" do
731
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]], :graph_join_type=>:inner
732
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id 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)'
733
+ end
734
+
735
+ it "should respect the association's :join_type option on through" do
736
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id, :join_type=>:natural}, [:albums_tags, :album_id, :tag_id]], :graph_join_type=>:inner
737
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) NATURAL 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)'
738
+ end
739
+
740
+ it "should respect the association's :conditions option" do
741
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :conditions=>{:a=>32}
742
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON ((tags.id = albums_tags.tag_id) AND (tags.a = 32))'
743
+ end
744
+
745
+ it "should respect the association's :graph_conditions option" do
746
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :graph_conditions=>{:a=>42}
747
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON ((tags.id = albums_tags.tag_id) AND (tags.a = 42))'
748
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :graph_conditions=>{:a=>42}, :conditions=>{:a=>32}
749
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON ((tags.id = albums_tags.tag_id) AND (tags.a = 42))'
750
+ end
751
+
752
+ it "should respect the association's :conditions option on through" do
753
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id, :conditions=>{:a=>42}}, [:albums_tags, :album_id, :tag_id]]
754
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON ((albums.id = albums_artists.album_id) AND (albums.a = 42)) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)'
755
+ end
756
+
757
+ it "should respect the association's :graph_block option" do
758
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :graph_block=>proc{|ja,lja,js| {:active.qualify(ja)=>true}}
759
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON ((tags.id = albums_tags.tag_id) AND (tags.active IS TRUE))'
760
+ end
761
+
762
+ it "should respect the association's :block option on through" do
763
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id, :block=>proc{|ja,lja,js| {:active.qualify(ja)=>true}}}, [:albums_tags, :album_id, :tag_id]]
764
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON ((albums.id = albums_artists.album_id) AND (albums.active IS TRUE)) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)'
765
+ end
766
+
767
+ it "should respect the association's :graph_only_conditions option" do
768
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :graph_only_conditions=>{:a=>32}
769
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.a = 32)'
770
+ end
771
+
772
+ it "should respect the association's :only_conditions option on through" do
773
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id, :only_conditions=>{:a=>42}}, [:albums_tags, :album_id, :tag_id]]
774
+ @c1.eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.a = 42) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id)'
775
+ end
776
+
777
+ it "should create unique table aliases for all associations" do
778
+ @c1.eager_graph(:artists=>{:artists=>:artists}).sql.should == "SELECT artists.id, artists_0.id AS artists_0_id, artists_1.id AS artists_1_id, artists_2.id AS artists_2_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.album_id = albums.id) LEFT OUTER JOIN artists AS artists_0 ON (artists_0.id = albums_artists_0.artist_id) LEFT OUTER JOIN albums_artists AS albums_artists_1 ON (albums_artists_1.artist_id = artists_0.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_1.album_id) LEFT OUTER JOIN albums_artists AS albums_artists_2 ON (albums_artists_2.album_id = albums_0.id) LEFT OUTER JOIN artists AS artists_1 ON (artists_1.id = albums_artists_2.artist_id) LEFT OUTER JOIN albums_artists AS albums_artists_3 ON (albums_artists_3.artist_id = artists_1.id) LEFT OUTER JOIN albums AS albums_1 ON (albums_1.id = albums_artists_3.album_id) LEFT OUTER JOIN albums_artists AS albums_artists_4 ON (albums_artists_4.album_id = albums_1.id) LEFT OUTER JOIN artists AS artists_2 ON (artists_2.id = albums_artists_4.artist_id)"
779
+ end
780
+
781
+ it "should respect the association's :order" do
782
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :order=>[:blah1, :blah2]
783
+ @c1.order(:artists__blah2, :artists__blah3).eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) ORDER BY artists.blah2, artists.blah3, tags.blah1, tags.blah2'
784
+ end
785
+
786
+ it "should only qualify unqualified symbols, identifiers, or ordered versions in association's :order" do
787
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :order=>[:blah__id.identifier, :blah__id.identifier.desc, :blah__id.desc, :blah__id, :album_id, :album_id.desc, 1, 'RANDOM()'.lit, :a.qualify(:b)]
788
+ @c1.order(:artists__blah2, :artists__blah3).eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) ORDER BY artists.blah2, artists.blah3, tags.blah__id, tags.blah__id DESC, blah.id DESC, blah.id, tags.album_id, tags.album_id DESC, 1, RANDOM(), b.a'
789
+ end
790
+
791
+ it "should not respect the association's :order if :order_eager_graph is false" do
792
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :order=>[:blah1, :blah2], :order_eager_graph=>false
793
+ @c1.order(:artists__blah2, :artists__blah3).eager_graph(:tags).sql.should == 'SELECT artists.id, tags.id AS tags_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) ORDER BY artists.blah2, artists.blah3'
794
+ end
795
+
796
+ it "should add the associations :order for multiple associations" do
797
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :order=>[:blah1, :blah2]
798
+ @c1.many_through_many :albums, [[:albums_artists, :artist_id, :album_id]], :order=>[:blah3, :blah4]
799
+ @c1.eager_graph(:tags, :albums).sql.should == 'SELECT artists.id, tags.id AS tags_id, albums_0.id AS albums_0_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON (albums_artists_0.artist_id = artists.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id) ORDER BY tags.blah1, tags.blah2, albums_0.blah3, albums_0.blah4'
800
+ end
801
+
802
+ it "should add the association's :order for cascading associations" do
803
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]], :order=>[:blah1, :blah2]
804
+ Tag.many_through_many :tracks, [[:albums_tags, :tag_id, :album_id], [:albums, :id, :id]], :right_primary_key=>:album_id, :order=>[:blah3, :blah4]
805
+ @c1.eager_graph(:tags=>:tracks).sql.should == 'SELECT artists.id, tags.id AS tags_id, tracks.id AS tracks_id FROM artists LEFT OUTER JOIN albums_artists ON (albums_artists.artist_id = artists.id) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_tags AS albums_tags_0 ON (albums_tags_0.tag_id = tags.id) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_tags_0.album_id) LEFT OUTER JOIN tracks ON (tracks.album_id = albums_0.id) ORDER BY tags.blah1, tags.blah2, tracks.blah3, tracks.blah4'
806
+ end
807
+
808
+ it "should use the correct qualifier when graphing multiple tables with extra conditions" do
809
+ @c1.many_through_many :tags, [{:table=>:albums_artists, :left=>:artist_id, :right=>:album_id, :conditions=>{:a=>:b}}, {:table=>:albums, :left=>:id, :right=>:id}, [:albums_tags, :album_id, :tag_id]]
810
+ @c1.many_through_many :albums, [{:table=>:albums_artists, :left=>:artist_id, :right=>:album_id, :conditions=>{:c=>:d}}]
811
+ @c1.eager_graph(:tags, :albums).sql.should == 'SELECT artists.id, tags.id AS tags_id, albums_0.id AS albums_0_id FROM artists LEFT OUTER JOIN albums_artists ON ((albums_artists.artist_id = artists.id) AND (albums_artists.a = artists.b)) LEFT OUTER JOIN albums ON (albums.id = albums_artists.album_id) LEFT OUTER JOIN albums_tags ON (albums_tags.album_id = albums.id) LEFT OUTER JOIN tags ON (tags.id = albums_tags.tag_id) LEFT OUTER JOIN albums_artists AS albums_artists_0 ON ((albums_artists_0.artist_id = artists.id) AND (albums_artists_0.c = artists.d)) LEFT OUTER JOIN albums AS albums_0 ON (albums_0.id = albums_artists_0.album_id)'
812
+ end
813
+ end