sequel 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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