sequel 3.27.0 → 3.28.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 (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)'