sequel 3.27.0 → 3.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/CHANGELOG +96 -0
  2. data/README.rdoc +2 -2
  3. data/Rakefile +1 -1
  4. data/doc/association_basics.rdoc +48 -0
  5. data/doc/opening_databases.rdoc +29 -5
  6. data/doc/prepared_statements.rdoc +1 -0
  7. data/doc/release_notes/3.28.0.txt +304 -0
  8. data/doc/testing.rdoc +42 -0
  9. data/doc/transactions.rdoc +97 -0
  10. data/lib/sequel/adapters/db2.rb +95 -65
  11. data/lib/sequel/adapters/firebird.rb +25 -219
  12. data/lib/sequel/adapters/ibmdb.rb +440 -0
  13. data/lib/sequel/adapters/jdbc.rb +12 -0
  14. data/lib/sequel/adapters/jdbc/as400.rb +0 -7
  15. data/lib/sequel/adapters/jdbc/db2.rb +49 -0
  16. data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
  17. data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
  18. data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
  19. data/lib/sequel/adapters/mysql.rb +10 -15
  20. data/lib/sequel/adapters/odbc.rb +1 -2
  21. data/lib/sequel/adapters/odbc/db2.rb +5 -5
  22. data/lib/sequel/adapters/postgres.rb +71 -11
  23. data/lib/sequel/adapters/shared/db2.rb +290 -0
  24. data/lib/sequel/adapters/shared/firebird.rb +214 -0
  25. data/lib/sequel/adapters/shared/mssql.rb +18 -75
  26. data/lib/sequel/adapters/shared/mysql.rb +13 -0
  27. data/lib/sequel/adapters/shared/postgres.rb +52 -36
  28. data/lib/sequel/adapters/shared/sqlite.rb +32 -36
  29. data/lib/sequel/adapters/sqlite.rb +4 -8
  30. data/lib/sequel/adapters/tinytds.rb +7 -3
  31. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
  32. data/lib/sequel/core.rb +1 -1
  33. data/lib/sequel/database/connecting.rb +1 -1
  34. data/lib/sequel/database/misc.rb +6 -5
  35. data/lib/sequel/database/query.rb +1 -1
  36. data/lib/sequel/database/schema_generator.rb +2 -1
  37. data/lib/sequel/dataset/actions.rb +149 -33
  38. data/lib/sequel/dataset/features.rb +44 -7
  39. data/lib/sequel/dataset/misc.rb +9 -1
  40. data/lib/sequel/dataset/prepared_statements.rb +2 -2
  41. data/lib/sequel/dataset/query.rb +63 -10
  42. data/lib/sequel/dataset/sql.rb +22 -5
  43. data/lib/sequel/model.rb +3 -3
  44. data/lib/sequel/model/associations.rb +250 -27
  45. data/lib/sequel/model/base.rb +10 -16
  46. data/lib/sequel/plugins/many_through_many.rb +34 -2
  47. data/lib/sequel/plugins/prepared_statements_with_pk.rb +1 -1
  48. data/lib/sequel/sql.rb +94 -51
  49. data/lib/sequel/version.rb +1 -1
  50. data/spec/adapters/db2_spec.rb +146 -0
  51. data/spec/adapters/postgres_spec.rb +74 -6
  52. data/spec/adapters/spec_helper.rb +1 -0
  53. data/spec/adapters/sqlite_spec.rb +11 -0
  54. data/spec/core/database_spec.rb +7 -0
  55. data/spec/core/dataset_spec.rb +180 -17
  56. data/spec/core/expression_filters_spec.rb +107 -41
  57. data/spec/core/spec_helper.rb +11 -0
  58. data/spec/extensions/many_through_many_spec.rb +115 -1
  59. data/spec/extensions/prepared_statements_with_pk_spec.rb +3 -3
  60. data/spec/integration/associations_test.rb +193 -15
  61. data/spec/integration/database_test.rb +4 -2
  62. data/spec/integration/dataset_test.rb +215 -19
  63. data/spec/integration/plugin_test.rb +8 -5
  64. data/spec/integration/prepared_statement_test.rb +91 -98
  65. data/spec/integration/schema_test.rb +27 -11
  66. data/spec/integration/spec_helper.rb +10 -0
  67. data/spec/integration/type_test.rb +2 -2
  68. data/spec/model/association_reflection_spec.rb +91 -0
  69. data/spec/model/associations_spec.rb +13 -0
  70. data/spec/model/base_spec.rb +8 -21
  71. data/spec/model/eager_loading_spec.rb +243 -9
  72. data/spec/model/model_spec.rb +15 -2
  73. metadata +16 -4
@@ -6,18 +6,21 @@ describe "Database schema parser" do
6
6
  @iom = INTEGRATION_DB.identifier_output_method
7
7
  @iim = INTEGRATION_DB.identifier_input_method
8
8
  @defsch = INTEGRATION_DB.default_schema
9
+ @qi = INTEGRATION_DB.quote_identifiers?
9
10
  clear_sqls
10
11
  end
11
12
  after do
12
13
  INTEGRATION_DB.identifier_output_method = @iom
13
14
  INTEGRATION_DB.identifier_input_method = @iim
14
15
  INTEGRATION_DB.default_schema = @defsch
16
+ INTEGRATION_DB.quote_identifiers = @qi
15
17
  INTEGRATION_DB.drop_table(:items) if INTEGRATION_DB.table_exists?(:items)
16
18
  end
17
19
 
18
20
  specify "should handle a database with a identifier_output_method" do
19
21
  INTEGRATION_DB.identifier_output_method = :reverse
20
22
  INTEGRATION_DB.identifier_input_method = :reverse
23
+ INTEGRATION_DB.quote_identifiers = true
21
24
  INTEGRATION_DB.default_schema = nil if INTEGRATION_DB.default_schema
22
25
  INTEGRATION_DB.create_table!(:items){Integer :number}
23
26
  INTEGRATION_DB.schema(:items, :reload=>true).should be_a_kind_of(Array)
@@ -85,7 +88,7 @@ describe "Database schema parser" do
85
88
  INTEGRATION_DB.schema(:items).first.last[:ruby_default].should == 'blah'
86
89
  end
87
90
 
88
- specify "should parse types from the schema properly" do
91
+ cspecify "should parse types from the schema properly", [:jdbc, :db2] do
89
92
  INTEGRATION_DB.create_table!(:items){Integer :number}
90
93
  INTEGRATION_DB.schema(:items).first.last[:type].should == :integer
91
94
  INTEGRATION_DB.create_table!(:items){Fixnum :number}
@@ -233,7 +236,7 @@ describe "Database schema modifiers" do
233
236
  specify "should add columns to tables correctly" do
234
237
  @db.create_table!(:items){Integer :number}
235
238
  @ds.insert(:number=>10)
236
- @db.alter_table(:items){add_column :name, :text}
239
+ @db.alter_table(:items){add_column :name, String}
237
240
  @db.schema(:items, :reload=>true).map{|x| x.first}.should == [:number, :name]
238
241
  @ds.columns!.should == [:number, :name]
239
242
  @ds.all.should == [{:number=>10, :name=>nil}]
@@ -296,7 +299,7 @@ describe "Database schema modifiers" do
296
299
  proc{@ds.insert(:n=>nil)}.should raise_error(Sequel::DatabaseError)
297
300
  end
298
301
 
299
- specify "should set column NULL/NOT NULL correctly" do
302
+ cspecify "should set column NULL/NOT NULL correctly", [:jdbc, :db2] do
300
303
  @db.create_table!(:items, :engine=>:InnoDB){Integer :id}
301
304
  @ds.insert(:id=>10)
302
305
  @db.alter_table(:items){set_column_allow_null :id, false}
@@ -318,7 +321,7 @@ describe "Database schema modifiers" do
318
321
  @ds.all.should == [{:id=>10}, {:id=>20}]
319
322
  end
320
323
 
321
- specify "should set column types correctly" do
324
+ cspecify "should set column types correctly", [:jdbc, :db2] do
322
325
  @db.create_table!(:items){Integer :id}
323
326
  @ds.insert(:id=>10)
324
327
  @db.alter_table(:items){set_column_type :id, String}
@@ -328,7 +331,7 @@ describe "Database schema modifiers" do
328
331
  @ds.all.should == [{:id=>"10"}, {:id=>"20"}]
329
332
  end
330
333
 
331
- cspecify "should add unique constraints and foreign key table constraints correctly", :sqlite do
334
+ cspecify "should add unnamed unique constraints and foreign key table constraints correctly", :sqlite do
332
335
  @db.create_table!(:items, :engine=>:InnoDB){Integer :id; Integer :item_id}
333
336
  @db.alter_table(:items) do
334
337
  add_unique_constraint [:item_id, :id]
@@ -341,6 +344,19 @@ describe "Database schema modifiers" do
341
344
  proc{@ds.insert(1, 2)}.should raise_error
342
345
  end
343
346
 
347
+ cspecify "should add named unique constraints and foreign key table constraints correctly", :sqlite do
348
+ @db.create_table!(:items, :engine=>:InnoDB){Integer :id, :null=>false; Integer :item_id, :null=>false}
349
+ @db.alter_table(:items) do
350
+ add_unique_constraint [:item_id, :id], :name=>:unique_iii
351
+ add_foreign_key [:id, :item_id], :items, :key=>[:item_id, :id], :name=>:fk_iii
352
+ end
353
+ @db.schema(:items, :reload=>true).map{|x| x.first}.should == [:id, :item_id]
354
+ @ds.columns!.should == [:id, :item_id]
355
+ proc{@ds.insert(1, 1)}.should_not raise_error
356
+ proc{@ds.insert(1, 1)}.should raise_error
357
+ proc{@ds.insert(1, 2)}.should raise_error
358
+ end
359
+
344
360
  cspecify "should drop unique constraints and foreign key table constraints correctly", :sqlite do
345
361
  @db.create_table!(:items) do
346
362
  Integer :id
@@ -358,7 +374,7 @@ describe "Database schema modifiers" do
358
374
  proc{@ds.insert(1, 2)}.should_not raise_error
359
375
  end
360
376
 
361
- cspecify "should remove columns from tables correctly", :h2, :mssql do
377
+ cspecify "should remove columns from tables correctly", :h2, :mssql, [:jdbc, :db2] do
362
378
  @db.create_table!(:items) do
363
379
  primary_key :id
364
380
  String :name
@@ -379,7 +395,7 @@ describe "Database schema modifiers" do
379
395
  @ds.columns!.should == [:id]
380
396
  end
381
397
 
382
- specify "should remove multiple columns in a single alter_table block" do
398
+ cspecify "should remove multiple columns in a single alter_table block", [:jdbc, :db2] do
383
399
  @db.create_table!(:items) do
384
400
  primary_key :id
385
401
  String :name
@@ -417,10 +433,10 @@ describe "Database#tables" do
417
433
  clear_sqls
418
434
  end
419
435
  after do
420
- @db.drop_view :sequel_test_view
421
- @db.drop_table :sequel_test_table
422
436
  @db.identifier_output_method = @iom
423
437
  @db.identifier_input_method = @iim
438
+ @db.drop_view :sequel_test_view
439
+ @db.drop_table :sequel_test_table
424
440
  end
425
441
 
426
442
  specify "should return an array of symbols" do
@@ -459,10 +475,10 @@ describe "Database#views" do
459
475
  clear_sqls
460
476
  end
461
477
  after do
462
- @db.drop_view :sequel_test_view
463
- @db.drop_table :sequel_test_table
464
478
  @db.identifier_output_method = @iom
465
479
  @db.identifier_input_method = @iim
480
+ @db.drop_view :sequel_test_view
481
+ @db.drop_table :sequel_test_table
466
482
  end
467
483
 
468
484
  specify "should return an array of symbols" do
@@ -49,6 +49,7 @@ end
49
49
  end
50
50
 
51
51
  def self.cspecify(message, *checked, &block)
52
+ return specify(message, &block) if ENV['SEQUEL_NO_PENDING']
52
53
  pending = false
53
54
  checked.each do |c|
54
55
  case c
@@ -89,3 +90,12 @@ if defined?(INTEGRATION_DB) || defined?(INTEGRATION_URL) || ENV['SEQUEL_INTEGRAT
89
90
  else
90
91
  INTEGRATION_DB = Sequel.sqlite('', :quote_identifiers=>false)
91
92
  end
93
+
94
+ if INTEGRATION_DB.adapter_scheme == :ibmdb
95
+ def INTEGRATION_DB.drop_table(*tables)
96
+ super
97
+ rescue Sequel::DatabaseError
98
+ disconnect
99
+ super
100
+ end
101
+ end
@@ -6,7 +6,7 @@ describe "Supported types" do
6
6
  INTEGRATION_DB[:items]
7
7
  end
8
8
 
9
- cspecify "should support casting correctly", [:sqlite, :sqlite] do
9
+ specify "should support casting correctly" do
10
10
  ds = create_items_table_with_column(:number, Integer)
11
11
  ds.insert(:number => 1)
12
12
  ds.select(:number.cast_string.as(:n)).map(:n).should == %w'1'
@@ -100,7 +100,7 @@ describe "Supported types" do
100
100
  ds.first[:name].should be_a_kind_of(::Sequel::SQL::Blob)
101
101
  end
102
102
 
103
- cspecify "should support generic boolean type", [:do, :sqlite], [:jdbc, :sqlite], [:odbc, :mssql] do
103
+ cspecify "should support generic boolean type", [:do, :sqlite], [:jdbc, :sqlite], [:jdbc, :db2], [:odbc, :mssql] do
104
104
  ds = create_items_table_with_column(:number, TrueClass)
105
105
  ds.insert(:number => true)
106
106
  ds.all.should == [{:number=>true}]
@@ -184,6 +184,7 @@ describe Sequel::Model::Associations::AssociationReflection, "#remove_before_des
184
184
  @c.many_to_many :cs, :class=>@c
185
185
  @c.association_reflection(:cs).remove_before_destroy?.should be_true
186
186
  end
187
+
187
188
  it "should be false for one_to_one and one_to_many associations" do
188
189
  @c.one_to_one :c, :class=>@c
189
190
  @c.association_reflection(:c).remove_before_destroy?.should be_false
@@ -192,3 +193,93 @@ describe Sequel::Model::Associations::AssociationReflection, "#remove_before_des
192
193
  end
193
194
  end
194
195
 
196
+ describe Sequel::Model::Associations::AssociationReflection, "#eager_limit_strategy" do
197
+ before do
198
+ @c = Class.new(Sequel::Model(:a))
199
+ end
200
+ after do
201
+ Sequel::Model.default_eager_limit_strategy = nil
202
+ end
203
+
204
+ it "should be nil by default for *_one associations" do
205
+ @c.many_to_one :c, :class=>@c
206
+ @c.association_reflection(:c).eager_limit_strategy.should be_nil
207
+ @c.one_to_one :c, :class=>@c
208
+ @c.association_reflection(:c).eager_limit_strategy.should be_nil
209
+ end
210
+
211
+ it "should be :ruby by default for *_many associations" do
212
+ @c.one_to_many :cs, :class=>@c, :limit=>1
213
+ @c.association_reflection(:cs).eager_limit_strategy.should == :ruby
214
+ @c.many_to_many :cs, :class=>@c, :limit=>1
215
+ @c.association_reflection(:cs).eager_limit_strategy.should == :ruby
216
+ end
217
+
218
+ it "should be nil for many_to_one associations" do
219
+ @c.many_to_one :c, :class=>@c, :eager_limit_strategy=>true
220
+ @c.association_reflection(:c).eager_limit_strategy.should be_nil
221
+ @c.many_to_one :c, :class=>@c, :eager_limit_strategy=>:distinct_on
222
+ @c.association_reflection(:c).eager_limit_strategy.should be_nil
223
+ end
224
+
225
+ it "should be a symbol for other associations if given a symbol" do
226
+ @c.one_to_one :c, :class=>@c, :eager_limit_strategy=>:distinct_on
227
+ @c.association_reflection(:c).eager_limit_strategy.should == :distinct_on
228
+ @c.one_to_many :cs, :class=>@c, :eager_limit_strategy=>:window_function, :limit=>1
229
+ @c.association_reflection(:cs).eager_limit_strategy.should == :window_function
230
+ end
231
+
232
+ it "should use :distinct_on for one_to_one associations if picking and the association dataset supports ordered distinct on" do
233
+ @c.dataset.meta_def(:supports_ordered_distinct_on?){true}
234
+ @c.one_to_one :c, :class=>@c, :eager_limit_strategy=>true
235
+ @c.association_reflection(:c).eager_limit_strategy.should == :distinct_on
236
+ end
237
+
238
+ it "should use :window_function for associations if picking and the association dataset supports window functions" do
239
+ @c.dataset.meta_def(:supports_window_functions?){true}
240
+ @c.one_to_one :c, :class=>@c, :eager_limit_strategy=>true
241
+ @c.association_reflection(:c).eager_limit_strategy.should == :window_function
242
+ @c.one_to_many :cs, :class=>@c, :eager_limit_strategy=>true, :limit=>1
243
+ @c.association_reflection(:cs).eager_limit_strategy.should == :window_function
244
+ @c.many_to_many :cs, :class=>@c, :eager_limit_strategy=>true, :limit=>1
245
+ @c.association_reflection(:cs).eager_limit_strategy.should == :window_function
246
+ end
247
+
248
+ it "should use :ruby for *_many associations if picking and the association dataset doesn't window functions" do
249
+ @c.one_to_many :cs, :class=>@c, :eager_limit_strategy=>true, :limit=>1
250
+ @c.association_reflection(:cs).eager_limit_strategy.should == :ruby
251
+ @c.many_to_many :cs, :class=>@c, :eager_limit_strategy=>true, :limit=>1
252
+ @c.association_reflection(:cs).eager_limit_strategy.should == :ruby
253
+ end
254
+
255
+ it "should respect Model.default_eager_limit_strategy to *_many associations" do
256
+ Sequel::Model.default_eager_limit_strategy = :window_function
257
+ Sequel::Model.default_eager_limit_strategy.should == :window_function
258
+ c = Class.new(Sequel::Model)
259
+ c.dataset = :a
260
+ c.default_eager_limit_strategy.should == :window_function
261
+ c.one_to_many :cs, :class=>c, :limit=>1
262
+ c.association_reflection(:cs).eager_limit_strategy.should == :window_function
263
+ c.many_to_many :cs, :class=>c, :limit=>1
264
+ c.association_reflection(:cs).eager_limit_strategy.should == :window_function
265
+
266
+ Sequel::Model.default_eager_limit_strategy = true
267
+ c = Class.new(Sequel::Model)
268
+ c.dataset = :a
269
+ c.one_to_many :cs, :class=>c, :limit=>1
270
+ c.association_reflection(:cs).eager_limit_strategy.should == :ruby
271
+ c.dataset.meta_def(:supports_window_functions?){true}
272
+ c.many_to_many :cs, :class=>c, :limit=>1
273
+ c.association_reflection(:cs).eager_limit_strategy.should == :window_function
274
+
275
+ c.default_eager_limit_strategy = :correlated_subquery
276
+ c.many_to_many :cs, :class=>c, :limit=>1
277
+ c.association_reflection(:cs).eager_limit_strategy.should == :correlated_subquery
278
+ end
279
+
280
+ it "should ignore Model.default_eager_limit_strategy for one_to_one associations" do
281
+ @c.default_eager_limit_strategy = :correlated_subquery
282
+ @c.one_to_one :c, :class=>@c
283
+ @c.association_reflection(:c).eager_limit_strategy.should be_nil
284
+ end
285
+ end
@@ -560,6 +560,19 @@ describe Sequel::Model, "many_to_one" do
560
560
  parent.pk.should == 20
561
561
  end
562
562
 
563
+ it "should support after_load association callback that changes the cached object" do
564
+ h = []
565
+ @c2.many_to_one :parent, :class => @c2, :after_load=>:al
566
+ @c2.class_eval do
567
+ def al(v)
568
+ associations[:parent] = :foo
569
+ end
570
+ end
571
+ p = @c2.load(:id=>10, :parent_id=>20)
572
+ p.parent.should == :foo
573
+ p.associations[:parent].should == :foo
574
+ end
575
+
563
576
  it "should raise error and not call internal add or remove method if before callback returns false, even if raise_on_save_failure is false" do
564
577
  # The reason for this is that assignment in ruby always returns the argument instead of the result
565
578
  # of the method, so we can't return nil to signal that the association callback prevented the modification
@@ -302,7 +302,7 @@ describe Sequel::Model, ".(allowed|restricted)_columns " do
302
302
  before do
303
303
  @c = Class.new(Sequel::Model(:blahblah)) do
304
304
  columns :x, :y, :z
305
- def _refresh(ds)
305
+ def _save_refresh
306
306
  self
307
307
  end
308
308
  end
@@ -544,47 +544,34 @@ describe "Model datasets #with_pk" do
544
544
 
545
545
  it "should return the first record where the primary key matches" do
546
546
  @ds.with_pk(1).should == @c.load(:id=>1)
547
- @sqls.should == ["SELECT * FROM a WHERE (id = 1) LIMIT 1"]
547
+ @sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"]
548
548
  end
549
549
 
550
550
  it "should handle existing filters" do
551
551
  @ds.filter(:a=>2).with_pk(1)
552
- @sqls.should == ["SELECT * FROM a WHERE ((a = 2) AND (id = 1)) LIMIT 1"]
552
+ @sqls.should == ["SELECT * FROM a WHERE ((a = 2) AND (a.id = 1)) LIMIT 1"]
553
553
  end
554
554
 
555
555
  it "should work with string values" do
556
556
  @ds.with_pk("foo")
557
- @sqls.should == ["SELECT * FROM a WHERE (id = 'foo') LIMIT 1"]
557
+ @sqls.should == ["SELECT * FROM a WHERE (a.id = 'foo') LIMIT 1"]
558
558
  end
559
559
 
560
560
  it "should handle an array for composite primary keys" do
561
561
  @c.set_primary_key :id1, :id2
562
562
  @ds.with_pk([1, 2])
563
- @sqls.should == ["SELECT * FROM a WHERE ((id1 = 1) AND (id2 = 2)) LIMIT 1"]
564
- end
565
-
566
- it "should raise an error if a single primary key is given when a composite primary key should be used" do
567
- @c.set_primary_key :id1, :id2
568
- proc{@ds.with_pk(1)}.should raise_error(Sequel::Error)
569
- end
570
-
571
- it "should raise an error if a composite primary key is given when a single primary key should be used" do
572
- proc{@ds.with_pk([1, 2])}.should raise_error(Sequel::Error)
573
- end
574
-
575
- it "should raise an error if the wrong number of composite keys is given" do
576
- @c.set_primary_key :id1, :id2
577
- proc{@ds.with_pk([1, 2, 3])}.should raise_error(Sequel::Error)
563
+ ["SELECT * FROM a WHERE ((a.id1 = 1) AND (a.id2 = 2)) LIMIT 1",
564
+ "SELECT * FROM a WHERE ((a.id2 = 2) AND (a.id1 = 1)) LIMIT 1"].should include(@sqls.first)
565
+ @sqls.length.should == 1
578
566
  end
579
567
 
580
568
  it "should have #[] consider an integer as a primary key lookup" do
581
569
  @ds[1].should == @c.load(:id=>1)
582
- @sqls.should == ["SELECT * FROM a WHERE (id = 1) LIMIT 1"]
570
+ @sqls.should == ["SELECT * FROM a WHERE (a.id = 1) LIMIT 1"]
583
571
  end
584
572
 
585
573
  it "should not have #[] consider a string as a primary key lookup" do
586
574
  @ds['foo'].should == @c.load(:id=>1)
587
575
  @sqls.should == ["SELECT * FROM a WHERE (foo) LIMIT 1"]
588
576
  end
589
-
590
577
  end
@@ -136,18 +136,68 @@ describe Sequel::Model, "#eager" do
136
136
  MODEL_DB.sqls.length.should == 2
137
137
  end
138
138
 
139
+ it "should eagerly load a single one_to_one association using the :distinct_on strategy" do
140
+ EagerTrack.dataset.meta_def(:supports_distinct_on?){true}
141
+ EagerAlbum.one_to_one :track, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>true
142
+ a = EagerAlbum.eager(:track).all
143
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
144
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT DISTINCT ON (tracks.album_id) * FROM tracks WHERE (tracks.album_id IN (1)) ORDER BY tracks.album_id']
145
+ a.first.track.should == EagerTrack.load(:id => 3, :album_id=>1)
146
+ MODEL_DB.sqls.length.should == 2
147
+ end
148
+
149
+ it "should eagerly load a single one_to_one association using the :window_function strategy" do
150
+ EagerTrack.dataset.meta_def(:supports_window_functions?){true}
151
+ EagerAlbum.one_to_one :track, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>true, :order=>:name
152
+ a = EagerAlbum.eager(:track).all
153
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
154
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM (SELECT *, row_number() OVER (PARTITION BY tracks.album_id ORDER BY name) AS x_sequel_row_number_x FROM tracks WHERE (tracks.album_id IN (1))) AS t1 WHERE (x_sequel_row_number_x = 1)']
155
+ a.first.track.should == EagerTrack.load(:id => 3, :album_id=>1)
156
+ MODEL_DB.sqls.length.should == 2
157
+ end
158
+
159
+ it "should eagerly load a single one_to_one association using the :correlated_subquery strategy" do
160
+ EagerAlbum.one_to_one :track, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>:correlated_subquery, :order=>:name
161
+ a = EagerAlbum.eager(:track).all
162
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
163
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM tracks WHERE ((tracks.album_id IN (1)) AND (tracks.id IN (SELECT t1.id FROM tracks AS t1 WHERE (t1.album_id = tracks.album_id) ORDER BY name LIMIT 1))) ORDER BY name']
164
+ a.first.track.should == EagerTrack.load(:id => 3, :album_id=>1)
165
+ MODEL_DB.sqls.length.should == 2
166
+ end
167
+
168
+ it "should handle qualified order clauses when eagerly loading a single one_to_one association using the :correlated_subquery strategy" do
169
+ EagerAlbum.one_to_one :track, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>:correlated_subquery, :order=>[:tracks__name, :tracks__name.desc, :name.qualify(:tracks), :name.qualify(:t), 1]
170
+ a = EagerAlbum.eager(:track).all
171
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
172
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM tracks WHERE ((tracks.album_id IN (1)) AND (tracks.id IN (SELECT t1.id FROM tracks AS t1 WHERE (t1.album_id = tracks.album_id) ORDER BY t1.name, t1.name DESC, t1.name, t.name, 1 LIMIT 1))) ORDER BY tracks.name, tracks.name DESC, tracks.name, t.name, 1']
173
+ a.first.track.should == EagerTrack.load(:id => 3, :album_id=>1)
174
+ MODEL_DB.sqls.length.should == 2
175
+ end
176
+
177
+ it "should handle qualified composite keys when eagerly loading a single one_to_one association using the :correlated_subquery strategy" do
178
+ c1 = Class.new(EagerAlbum)
179
+ c2 = Class.new(EagerTrack)
180
+ c1.set_primary_key [:id, :band_id]
181
+ c2.set_primary_key [:id, :album_id]
182
+ c1.one_to_one :track, :class=>c2, :key=>[:album_id, :id], :eager_limit_strategy=>:correlated_subquery
183
+ c2.dataset.extend(Module.new do
184
+ def fetch_rows(sql)
185
+ MODEL_DB << sql
186
+ yield({:id => 2, :album_id=>1})
187
+ end
188
+ end)
189
+ a = c1.eager(:track).all
190
+ a.should == [c1.load(:id => 1, :band_id => 2)]
191
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM tracks WHERE (((tracks.album_id, tracks.id) IN ((1, 2))) AND ((tracks.id, tracks.album_id) IN (SELECT t1.id, t1.album_id FROM tracks AS t1 WHERE ((t1.album_id = tracks.album_id) AND (t1.id = tracks.id)) LIMIT 1)))']
192
+ a.first.track.should == c2.load(:id => 2, :album_id=>1)
193
+ MODEL_DB.sqls.length.should == 2
194
+ end
195
+
139
196
  it "should eagerly load a single one_to_many association" do
140
197
  a = EagerAlbum.eager(:tracks).all
141
- a.should be_a_kind_of(Array)
142
- a.size.should == 1
143
- a.first.should be_a_kind_of(EagerAlbum)
144
- a.first.values.should == {:id => 1, :band_id => 2}
198
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
145
199
  MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM tracks WHERE (tracks.album_id IN (1))']
146
- a = a.first
147
- a.tracks.should be_a_kind_of(Array)
148
- a.tracks.size.should == 1
149
- a.tracks.first.should be_a_kind_of(EagerTrack)
150
- a.tracks.first.values.should == {:id => 3, :album_id=>1}
200
+ a.first.tracks.should == [EagerTrack.load(:id => 3, :album_id=>1)]
151
201
  MODEL_DB.sqls.length.should == 2
152
202
  end
153
203
 
@@ -564,6 +614,135 @@ describe Sequel::Model, "#eager" do
564
614
  as.first.special_genres.should == [EagerGenre.load(:id=>5), EagerGenre.load(:id=>6)]
565
615
  end
566
616
 
617
+ it "should respect the :limit option on a one_to_many association" do
618
+ EagerAlbum.one_to_many :first_two_tracks, :class=>:EagerTrack, :key=>:album_id, :limit=>2
619
+ EagerTrack.dataset.extend(Module.new {
620
+ def fetch_rows(sql)
621
+ MODEL_DB.sqls << sql
622
+ yield({:album_id=>1, :id=>2})
623
+ yield({:album_id=>1, :id=>3})
624
+ yield({:album_id=>1, :id=>4})
625
+ end
626
+ })
627
+ as = EagerAlbum.eager(:first_two_tracks).all
628
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT * FROM tracks WHERE (tracks.album_id IN (1))"]
629
+ as.length.should == 1
630
+ as.first.first_two_tracks.should == [EagerTrack.load(:album_id=>1, :id=>2), EagerTrack.load(:album_id=>1, :id=>3)]
631
+
632
+ MODEL_DB.reset
633
+ EagerAlbum.one_to_many :first_two_tracks, :class=>:EagerTrack, :key=>:album_id, :limit=>[2,1]
634
+ as = EagerAlbum.eager(:first_two_tracks).all
635
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT * FROM tracks WHERE (tracks.album_id IN (1))"]
636
+ as.length.should == 1
637
+ as.first.first_two_tracks.should == [EagerTrack.load(:album_id=>1, :id=>3), EagerTrack.load(:album_id=>1, :id=>4)]
638
+ end
639
+
640
+ it "should respect the :limit option on a one_to_many association using the :window_function strategy" do
641
+ EagerTrack.dataset.meta_def(:supports_window_functions?){true}
642
+ EagerAlbum.one_to_many :tracks, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>true, :order=>:name, :limit=>2
643
+ a = EagerAlbum.eager(:tracks).all
644
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
645
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM (SELECT *, row_number() OVER (PARTITION BY tracks.album_id ORDER BY name) AS x_sequel_row_number_x FROM tracks WHERE (tracks.album_id IN (1))) AS t1 WHERE (x_sequel_row_number_x <= 2)']
646
+ a.first.tracks.should == [EagerTrack.load(:id => 3, :album_id=>1)]
647
+ MODEL_DB.sqls.length.should == 2
648
+ end
649
+
650
+ it "should respect the :limit option with an offset on a one_to_many association using the :window_function strategy" do
651
+ EagerTrack.dataset.meta_def(:supports_window_functions?){true}
652
+ EagerAlbum.one_to_many :tracks, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>true, :order=>:name, :limit=>[2, 1]
653
+ a = EagerAlbum.eager(:tracks).all
654
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
655
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM (SELECT *, row_number() OVER (PARTITION BY tracks.album_id ORDER BY name) AS x_sequel_row_number_x FROM tracks WHERE (tracks.album_id IN (1))) AS t1 WHERE ((x_sequel_row_number_x >= 2) AND (x_sequel_row_number_x < 4))']
656
+ a.first.tracks.should == [EagerTrack.load(:id => 3, :album_id=>1)]
657
+ MODEL_DB.sqls.length.should == 2
658
+ end
659
+
660
+ it "should respect the :limit option on a one_to_many association using the :correlated_subquery strategy" do
661
+ EagerAlbum.one_to_many :tracks, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>:correlated_subquery, :order=>:name, :limit=>2
662
+ a = EagerAlbum.eager(:tracks).all
663
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
664
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM tracks WHERE ((tracks.album_id IN (1)) AND (tracks.id IN (SELECT t1.id FROM tracks AS t1 WHERE (t1.album_id = tracks.album_id) ORDER BY name LIMIT 2))) ORDER BY name']
665
+ a.first.tracks.should == [EagerTrack.load(:id => 3, :album_id=>1)]
666
+ MODEL_DB.sqls.length.should == 2
667
+ end
668
+
669
+ it "should respect the :limit option with an offset on a one_to_many association using the :correlated_subquery strategy" do
670
+ EagerAlbum.one_to_many :tracks, :class=>'EagerTrack', :key=>:album_id, :eager_limit_strategy=>:correlated_subquery, :order=>:name, :limit=>[2, 1]
671
+ a = EagerAlbum.eager(:tracks).all
672
+ a.should == [EagerAlbum.load(:id => 1, :band_id => 2)]
673
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', 'SELECT * FROM tracks WHERE ((tracks.album_id IN (1)) AND (tracks.id IN (SELECT t1.id FROM tracks AS t1 WHERE (t1.album_id = tracks.album_id) ORDER BY name LIMIT 2 OFFSET 1))) ORDER BY name']
674
+ a.first.tracks.should == [EagerTrack.load(:id => 3, :album_id=>1)]
675
+ MODEL_DB.sqls.length.should == 2
676
+ end
677
+
678
+ it "should respect the limit option on a many_to_many association" do
679
+ EagerAlbum.many_to_many :first_two_genres, :class=>:EagerGenre, :left_primary_key=>:band_id, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :limit=>2
680
+ EagerGenre.dataset.extend(Module.new {
681
+ def fetch_rows(sql)
682
+ MODEL_DB.sqls << sql
683
+ yield({:x_foreign_key_x=>2, :id=>5})
684
+ yield({:x_foreign_key_x=>2, :id=>6})
685
+ yield({:x_foreign_key_x=>2, :id=>7})
686
+ end
687
+ })
688
+ as = EagerAlbum.eager(:first_two_genres).all
689
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT genres.*, ag.album_id AS x_foreign_key_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (2)))"]
690
+ as.length.should == 1
691
+ as.first.first_two_genres.should == [EagerGenre.load(:id=>5), EagerGenre.load(:id=>6)]
692
+
693
+ MODEL_DB.reset
694
+ EagerAlbum.many_to_many :first_two_genres, :class=>:EagerGenre, :left_primary_key=>:band_id, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :limit=>[2, 1]
695
+ as = EagerAlbum.eager(:first_two_genres).all
696
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT genres.*, ag.album_id AS x_foreign_key_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (2)))"]
697
+ as.length.should == 1
698
+ as.first.first_two_genres.should == [EagerGenre.load(:id=>6), EagerGenre.load(:id=>7)]
699
+ end
700
+
701
+ it "should respect the limit option on a many_to_many association using the :window_function strategy" do
702
+ EagerGenre.dataset.meta_def(:supports_window_functions?){true}
703
+ EagerAlbum.many_to_many :first_two_genres, :class=>:EagerGenre, :left_primary_key=>:band_id, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :eager_limit_strategy=>true, :limit=>2, :order=>:name
704
+ EagerGenre.dataset.extend(Module.new {
705
+ def fetch_rows(sql)
706
+ MODEL_DB.sqls << sql
707
+ yield({:x_foreign_key_x=>2, :id=>5})
708
+ yield({:x_foreign_key_x=>2, :id=>6})
709
+ end
710
+ })
711
+ as = EagerAlbum.eager(:first_two_genres).all
712
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT * FROM (SELECT genres.*, ag.album_id AS x_foreign_key_x, row_number() OVER (PARTITION BY ag.album_id ORDER BY name) AS x_sequel_row_number_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (2)))) AS t1 WHERE (x_sequel_row_number_x <= 2)"]
713
+ as.length.should == 1
714
+ as.first.first_two_genres.should == [EagerGenre.load(:id=>5), EagerGenre.load(:id=>6)]
715
+
716
+ MODEL_DB.reset
717
+ EagerAlbum.many_to_many :first_two_genres, :class=>:EagerGenre, :left_primary_key=>:band_id, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :eager_limit_strategy=>true, :limit=>[2, 1], :order=>:name
718
+ as = EagerAlbum.eager(:first_two_genres).all
719
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT * FROM (SELECT genres.*, ag.album_id AS x_foreign_key_x, row_number() OVER (PARTITION BY ag.album_id ORDER BY name) AS x_sequel_row_number_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (2)))) AS t1 WHERE ((x_sequel_row_number_x >= 2) AND (x_sequel_row_number_x < 4))"]
720
+ as.length.should == 1
721
+ as.first.first_two_genres.should == [EagerGenre.load(:id=>5), EagerGenre.load(:id=>6)]
722
+ end
723
+
724
+ it "should respect the limit option on a many_to_many association using the :correlated_subquery strategy" do
725
+ EagerAlbum.many_to_many :first_two_genres, :class=>:EagerGenre, :left_primary_key=>:band_id, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :eager_limit_strategy=>:correlated_subquery, :limit=>2, :order=>:name
726
+ EagerGenre.dataset.extend(Module.new {
727
+ def fetch_rows(sql)
728
+ MODEL_DB.sqls << sql
729
+ yield({:x_foreign_key_x=>2, :id=>5})
730
+ yield({:x_foreign_key_x=>2, :id=>6})
731
+ end
732
+ })
733
+ as = EagerAlbum.eager(:first_two_genres).all
734
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT genres.*, ag.album_id AS x_foreign_key_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (2))) WHERE (genres.id IN (SELECT t1.id FROM genres AS t1 INNER JOIN ag AS t2 ON ((t2.genre_id = t1.id) AND (t2.album_id = ag.album_id)) ORDER BY name LIMIT 2)) ORDER BY name"]
735
+ as.length.should == 1
736
+ as.first.first_two_genres.should == [EagerGenre.load(:id=>5), EagerGenre.load(:id=>6)]
737
+
738
+ MODEL_DB.reset
739
+ EagerAlbum.many_to_many :first_two_genres, :class=>:EagerGenre, :left_primary_key=>:band_id, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :eager_limit_strategy=>:correlated_subquery, :limit=>[2, 1], :order=>:name
740
+ as = EagerAlbum.eager(:first_two_genres).all
741
+ MODEL_DB.sqls.should == ['SELECT * FROM albums', "SELECT genres.*, ag.album_id AS x_foreign_key_x FROM genres INNER JOIN ag ON ((ag.genre_id = genres.id) AND (ag.album_id IN (2))) WHERE (genres.id IN (SELECT t1.id FROM genres AS t1 INNER JOIN ag AS t2 ON ((t2.genre_id = t1.id) AND (t2.album_id = ag.album_id)) ORDER BY name LIMIT 2 OFFSET 1)) ORDER BY name"]
742
+ as.length.should == 1
743
+ as.first.first_two_genres.should == [EagerGenre.load(:id=>5), EagerGenre.load(:id=>6)]
744
+ end
745
+
567
746
  it "should use the :eager_loader association option when eager loading" do
568
747
  EagerAlbum.many_to_one :special_band, :eager_loader=>(proc do |key_hash, records, assocs|
569
748
  item = EagerBand.filter(:album_id=>records.collect{|r| [r.pk, r.pk*2]}.flatten).order(:name).first
@@ -1340,6 +1519,27 @@ describe Sequel::Model, "#eager_graph" do
1340
1519
  as.first.inner_genres.should == [GraphGenre.load(:id=>5), GraphGenre.load(:id=>6)]
1341
1520
  end
1342
1521
 
1522
+ it "should respect composite primary keys for classes when eager loading" do
1523
+ c1 = Class.new(GraphAlbum)
1524
+ c2 = Class.new(GraphBand)
1525
+ c1.set_primary_key [:band_id, :id]
1526
+ c2.set_primary_key [:vocalist_id, :id]
1527
+ c1.many_to_many :sbands, :class=>c2, :left_key=>[:l1, :l2], :right_key=>[:r1, :r2], :join_table=>:b
1528
+ c2.one_to_many :salbums, :class=>c1, :key=>[:band_id, :id]
1529
+ ds = c1.eager_graph(:sbands=>:salbums)
1530
+ ds.sql.should == 'SELECT albums.id, albums.band_id, sbands.id AS sbands_id, sbands.vocalist_id, salbums.id AS salbums_id, salbums.band_id AS salbums_band_id FROM albums LEFT OUTER JOIN b ON ((b.l1 = albums.band_id) AND (b.l2 = albums.id)) LEFT OUTER JOIN bands AS sbands ON ((sbands.vocalist_id = b.r1) AND (sbands.id = b.r2)) LEFT OUTER JOIN albums AS salbums ON ((salbums.band_id = sbands.vocalist_id) AND (salbums.id = sbands.id))'
1531
+ def ds.fetch_rows(sql, &block)
1532
+ yield({:id=>3, :band_id=>2, :sbands_id=>5, :vocalist_id=>6, :salbums_id=>7, :salbums_band_id=>8})
1533
+ yield({:id=>3, :band_id=>2, :sbands_id=>5, :vocalist_id=>6, :salbums_id=>9, :salbums_band_id=>10})
1534
+ yield({:id=>3, :band_id=>2, :sbands_id=>6, :vocalist_id=>22, :salbums_id=>nil, :salbums_band_id=>nil})
1535
+ yield({:id=>7, :band_id=>8, :sbands_id=>nil, :vocalist_id=>nil, :salbums_id=>nil, :salbums_band_id=>nil})
1536
+ end
1537
+ as = ds.all
1538
+ as.should == [c1.load(:id=>3, :band_id=>2), c1.load(:id=>7, :band_id=>8)]
1539
+ as.map{|x| x.sbands}.should == [[c2.load(:id=>5, :vocalist_id=>6), c2.load(:id=>6, :vocalist_id=>22)], []]
1540
+ as.map{|x| x.sbands.map{|y| y.salbums}}.should == [[[c1.load(:id=>7, :band_id=>8), c1.load(:id=>9, :band_id=>10)], []], []]
1541
+ end
1542
+
1343
1543
  it "should respect the association's :graph_select option" do
1344
1544
  GraphAlbum.many_to_one :inner_band, :class=>'GraphBand', :key=>:band_id, :graph_select=>:vocalist_id
1345
1545
  GraphAlbum.eager_graph(:inner_band).sql.should == 'SELECT albums.id, albums.band_id, inner_band.vocalist_id FROM albums LEFT OUTER JOIN bands AS inner_band ON (inner_band.id = albums.band_id)'
@@ -1538,6 +1738,40 @@ describe Sequel::Model, "#eager_graph" do
1538
1738
  ds.sql.should == 'SELECT a.id, a_genres.id AS a_genres_id FROM (SELECT * FROM s.a INNER JOIN s.t USING (b_id)) AS a LEFT OUTER JOIN s.ag AS ag ON (ag.album_id = a.id) LEFT OUTER JOIN s.g AS a_genres ON (a_genres.id = ag.genre_id)'
1539
1739
  end
1540
1740
 
1741
+ it "should respect :after_load callbacks on associations when eager graphing" do
1742
+ GraphAlbum.many_to_one :al_band, :class=>GraphBand, :key=>:band_id, :after_load=>proc{|o, a| a.id *=2}
1743
+ GraphAlbum.one_to_many :al_tracks, :class=>GraphTrack, :key=>:album_id, :after_load=>proc{|o, os| os.each{|a| a.id *=2}}
1744
+ GraphAlbum.many_to_many :al_genres, :class=>GraphGenre, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :after_load=>proc{|o, os| os.each{|a| a.id *=2}}
1745
+ ds = GraphAlbum.eager_graph(:al_band, :al_tracks, :al_genres)
1746
+ ds.sql.should == "SELECT albums.id, albums.band_id, al_band.id AS al_band_id, al_band.vocalist_id, al_tracks.id AS al_tracks_id, al_tracks.album_id, al_genres.id AS al_genres_id FROM albums LEFT OUTER JOIN bands AS al_band ON (al_band.id = albums.band_id) LEFT OUTER JOIN tracks AS al_tracks ON (al_tracks.album_id = albums.id) LEFT OUTER JOIN ag ON (ag.album_id = albums.id) LEFT OUTER JOIN genres AS al_genres ON (al_genres.id = ag.genre_id)"
1747
+ def ds.fetch_rows(sql)
1748
+ yield({:id=>1, :band_id=>2, :al_band_id=>3, :vocalist_id=>4, :al_tracks_id=>5, :album_id=>6, :al_genres_id=>7})
1749
+ end
1750
+ a = ds.all.first
1751
+ a.should == GraphAlbum.load(:id => 1, :band_id => 2)
1752
+ a.al_band.should == GraphBand.load(:id=>6, :vocalist_id=>4)
1753
+ a.al_tracks.should == [GraphTrack.load(:id=>10, :album_id=>6)]
1754
+ a.al_genres.should == [GraphGenre.load(:id=>14)]
1755
+ end
1756
+
1757
+ it "should respect limits on associations when eager graphing" do
1758
+ GraphAlbum.many_to_one :al_band, :class=>GraphBand, :key=>:band_id
1759
+ GraphAlbum.one_to_many :al_tracks, :class=>GraphTrack, :key=>:album_id, :limit=>2
1760
+ GraphAlbum.many_to_many :al_genres, :class=>GraphGenre, :left_key=>:album_id, :right_key=>:genre_id, :join_table=>:ag, :limit=>2
1761
+ ds = GraphAlbum.eager_graph(:al_band, :al_tracks, :al_genres)
1762
+ ds.sql.should == "SELECT albums.id, albums.band_id, al_band.id AS al_band_id, al_band.vocalist_id, al_tracks.id AS al_tracks_id, al_tracks.album_id, al_genres.id AS al_genres_id FROM albums LEFT OUTER JOIN bands AS al_band ON (al_band.id = albums.band_id) LEFT OUTER JOIN tracks AS al_tracks ON (al_tracks.album_id = albums.id) LEFT OUTER JOIN ag ON (ag.album_id = albums.id) LEFT OUTER JOIN genres AS al_genres ON (al_genres.id = ag.genre_id)"
1763
+ def ds.fetch_rows(sql)
1764
+ yield({:id=>1, :band_id=>2, :al_band_id=>3, :vocalist_id=>4, :al_tracks_id=>5, :album_id=>6, :al_genres_id=>7})
1765
+ yield({:id=>1, :band_id=>2, :al_band_id=>8, :vocalist_id=>9, :al_tracks_id=>10, :album_id=>11, :al_genres_id=>12})
1766
+ yield({:id=>1, :band_id=>2, :al_band_id=>13, :vocalist_id=>14, :al_tracks_id=>15, :album_id=>16, :al_genres_id=>17})
1767
+ end
1768
+ a = ds.all.first
1769
+ a.should == GraphAlbum.load(:id => 1, :band_id => 2)
1770
+ a.al_band.should == GraphBand.load(:id=>3, :vocalist_id=>4)
1771
+ a.al_tracks.should == [GraphTrack.load(:id=>5, :album_id=>6), GraphTrack.load(:id=>10, :album_id=>11)]
1772
+ a.al_genres.should == [GraphGenre.load(:id=>7), GraphGenre.load(:id=>12)]
1773
+ end
1774
+
1541
1775
  it "should eagerly load a many_to_one association with a custom callback" do
1542
1776
  ds = GraphAlbum.eager_graph(:band => proc {|ds| ds.select_columns(:id)})
1543
1777
  ds.sql.should == 'SELECT albums.id, albums.band_id, band.id AS band_id_0 FROM albums LEFT OUTER JOIN (SELECT id FROM bands) AS band ON (band.id = albums.band_id)'