sequel 4.33.0 → 4.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +22 -0
  3. data/doc/release_notes/4.34.0.txt +86 -0
  4. data/doc/testing.rdoc +1 -0
  5. data/doc/validations.rdoc +12 -1
  6. data/lib/sequel/adapters/ado.rb +1 -1
  7. data/lib/sequel/adapters/amalgalite.rb +1 -1
  8. data/lib/sequel/adapters/cubrid.rb +1 -1
  9. data/lib/sequel/adapters/do.rb +1 -1
  10. data/lib/sequel/adapters/ibmdb.rb +1 -1
  11. data/lib/sequel/adapters/jdbc.rb +1 -1
  12. data/lib/sequel/adapters/mock.rb +1 -1
  13. data/lib/sequel/adapters/mysql.rb +1 -1
  14. data/lib/sequel/adapters/mysql2.rb +1 -1
  15. data/lib/sequel/adapters/odbc.rb +1 -1
  16. data/lib/sequel/adapters/oracle.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +1 -1
  18. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  19. data/lib/sequel/adapters/sqlanywhere.rb +1 -1
  20. data/lib/sequel/adapters/sqlite.rb +1 -1
  21. data/lib/sequel/adapters/swift.rb +1 -1
  22. data/lib/sequel/adapters/tinytds.rb +2 -2
  23. data/lib/sequel/connection_pool.rb +2 -0
  24. data/lib/sequel/connection_pool/sharded_single.rb +1 -1
  25. data/lib/sequel/connection_pool/sharded_threaded.rb +17 -4
  26. data/lib/sequel/connection_pool/single.rb +1 -1
  27. data/lib/sequel/connection_pool/threaded.rb +17 -4
  28. data/lib/sequel/database/misc.rb +5 -1
  29. data/lib/sequel/dataset.rb +4 -0
  30. data/lib/sequel/dataset/actions.rb +28 -15
  31. data/lib/sequel/extensions/columns_introspection.rb +1 -1
  32. data/lib/sequel/extensions/duplicate_columns_handler.rb +87 -0
  33. data/lib/sequel/extensions/migration.rb +9 -7
  34. data/lib/sequel/extensions/pg_range.rb +73 -14
  35. data/lib/sequel/model/base.rb +2 -2
  36. data/lib/sequel/plugins/dataset_associations.rb +21 -1
  37. data/lib/sequel/plugins/prepared_statements_safe.rb +2 -1
  38. data/lib/sequel/plugins/update_or_create.rb +1 -1
  39. data/lib/sequel/plugins/validation_helpers.rb +7 -0
  40. data/lib/sequel/version.rb +1 -1
  41. data/spec/adapters/postgres_spec.rb +14 -0
  42. data/spec/adapters/spec_helper.rb +6 -0
  43. data/spec/core/connection_pool_spec.rb +30 -3
  44. data/spec/core/database_spec.rb +2 -0
  45. data/spec/core/dataset_spec.rb +8 -0
  46. data/spec/extensions/dataset_associations_spec.rb +32 -0
  47. data/spec/extensions/duplicate_columns_handler_spec.rb +110 -0
  48. data/spec/extensions/pg_range_spec.rb +40 -0
  49. data/spec/extensions/prepared_statements_safe_spec.rb +1 -1
  50. data/spec/extensions/validation_helpers_spec.rb +11 -0
  51. data/spec/integration/associations_test.rb +22 -8
  52. data/spec/integration/dataset_test.rb +10 -0
  53. data/spec/integration/eager_loader_test.rb +1 -1
  54. data/spec/integration/plugin_test.rb +3 -3
  55. data/spec/integration/spec_helper.rb +4 -0
  56. metadata +6 -2
@@ -108,6 +108,7 @@ module Sequel
108
108
  # :identifier_output_method :: A string method symbol to call on identifiers coming from the database.
109
109
  # :logger :: A specific logger to use.
110
110
  # :loggers :: An array of loggers to use.
111
+ # :name :: A name to use for the Database object.
111
112
  # :preconnect :: Whether to automatically connect to the maximum number of servers.
112
113
  # :quote_identifiers :: Whether to quote identifiers.
113
114
  # :servers :: A hash specifying a server/shard specific options, keyed by shard symbol .
@@ -149,7 +150,10 @@ module Sequel
149
150
  Sequel.synchronize{::Sequel::DATABASES.push(self)}
150
151
  end
151
152
  Sequel::Database.run_after_initialize(self)
152
- @pool.send(:preconnect) if typecast_value_boolean(@opts[:preconnect]) && @pool.respond_to?(:preconnect, true)
153
+ if typecast_value_boolean(@opts[:preconnect]) && @pool.respond_to?(:preconnect, true)
154
+ concurrent = typecast_value_string(@opts[:preconnect]) == "concurrently"
155
+ @pool.send(:preconnect, concurrent)
156
+ end
153
157
  end
154
158
 
155
159
  # If a transaction is not currently in process, yield to the block immediately.
@@ -36,6 +36,10 @@ module Sequel
36
36
  include SQL::NumericMethods
37
37
  include SQL::OrderMethods
38
38
  include SQL::StringMethods
39
+
40
+ private
41
+
42
+ attr_writer :columns
39
43
  end
40
44
 
41
45
  require(%w"query actions features graph prepared_statements misc mutation sql placeholder_literalizer", 'dataset')
@@ -81,7 +81,7 @@ module Sequel
81
81
  # DB[:table].columns!
82
82
  # # => [:id, :name]
83
83
  def columns!
84
- @columns = nil
84
+ self.columns = nil
85
85
  columns
86
86
  end
87
87
 
@@ -558,7 +558,9 @@ module Sequel
558
558
  end
559
559
 
560
560
  # Returns a hash with key_column values as keys and value_column values as
561
- # values. Similar to to_hash, but only selects the columns given.
561
+ # values. Similar to to_hash, but only selects the columns given. Like
562
+ # to_hash, it accepts an optional :hash parameter, into which entries will
563
+ # be merged.
562
564
  #
563
565
  # DB[:table].select_hash(:id, :name) # SELECT id, name FROM table
564
566
  # # => {1=>'a', 2=>'b', ...}
@@ -572,12 +574,13 @@ module Sequel
572
574
  # When using this method, you must be sure that each expression has an alias
573
575
  # that Sequel can determine. Usually you can do this by calling the #as method
574
576
  # on the expression and providing an alias.
575
- def select_hash(key_column, value_column)
576
- _select_hash(:to_hash, key_column, value_column)
577
+ def select_hash(key_column, value_column, opts = OPTS)
578
+ _select_hash(:to_hash, key_column, value_column, opts)
577
579
  end
578
580
 
579
581
  # Returns a hash with key_column values as keys and an array of value_column values.
580
- # Similar to to_hash_groups, but only selects the columns given.
582
+ # Similar to to_hash_groups, but only selects the columns given. Like to_hash_groups,
583
+ # it accepts an optional :hash parameter, into which entries will be merged.
581
584
  #
582
585
  # DB[:table].select_hash_groups(:name, :id) # SELECT id, name FROM table
583
586
  # # => {'a'=>[1, 4, ...], 'b'=>[2, ...], ...}
@@ -591,8 +594,8 @@ module Sequel
591
594
  # When using this method, you must be sure that each expression has an alias
592
595
  # that Sequel can determine. Usually you can do this by calling the #as method
593
596
  # on the expression and providing an alias.
594
- def select_hash_groups(key_column, value_column)
595
- _select_hash(:to_hash_groups, key_column, value_column)
597
+ def select_hash_groups(key_column, value_column, opts = OPTS)
598
+ _select_hash(:to_hash_groups, key_column, value_column, opts)
596
599
  end
597
600
 
598
601
  # Selects the column given (either as an argument or as a block), and
@@ -715,10 +718,15 @@ module Sequel
715
718
  #
716
719
  # DB[:table].to_hash([:id, :name]) # SELECT * FROM table
717
720
  # # {[1, 'Jim']=>{:id=>1, :name=>'Jim'}, [2, 'Bob'=>{:id=>2, :name=>'Bob'}, ...}
718
- def to_hash(key_column, value_column = nil)
719
- h = {}
721
+ #
722
+ # This method accepts an optional :hash parameter (which can be a hash with
723
+ # a default value, a hash with a default proc, or any object that supports
724
+ # #[] and #[]=) into which entries will be merged. The default behavior is
725
+ # to start with a new, empty hash.
726
+ def to_hash(key_column, value_column = nil, opts = OPTS)
727
+ h = opts[:hash] || {}
720
728
  if value_column
721
- return naked.to_hash(key_column, value_column) if row_proc
729
+ return naked.to_hash(key_column, value_column, opts) if row_proc
722
730
  if value_column.is_a?(Array)
723
731
  if key_column.is_a?(Array)
724
732
  each{|r| h[r.values_at(*key_column)] = r.values_at(*value_column)}
@@ -758,10 +766,15 @@ module Sequel
758
766
  #
759
767
  # DB[:table].to_hash_groups([:first, :middle]) # SELECT * FROM table
760
768
  # # {['Jim', 'Bob']=>[{:id=>1, :first=>'Jim', :middle=>'Bob', :last=>'Smith'}, ...], ...}
761
- def to_hash_groups(key_column, value_column = nil)
762
- h = {}
769
+ #
770
+ # This method accepts an optional :hash parameter (which can be a hash with
771
+ # a default value, a hash with a default proc, or any object that supports
772
+ # #[] and #[]=) into which entries will be merged. The default behavior is
773
+ # to start with a new, empty hash.
774
+ def to_hash_groups(key_column, value_column = nil, opts = OPTS)
775
+ h = opts[:hash] || {}
763
776
  if value_column
764
- return naked.to_hash_groups(key_column, value_column) if row_proc
777
+ return naked.to_hash_groups(key_column, value_column, opts) if row_proc
765
778
  if value_column.is_a?(Array)
766
779
  if key_column.is_a?(Array)
767
780
  each{|r| (h[r.values_at(*key_column)] ||= []) << r.values_at(*value_column)}
@@ -896,9 +909,9 @@ module Sequel
896
909
  end
897
910
 
898
911
  # Internals of +select_hash+ and +select_hash_groups+
899
- def _select_hash(meth, key_column, value_column)
912
+ def _select_hash(meth, key_column, value_column, opts=OPTS)
900
913
  select(*(key_column.is_a?(Array) ? key_column : [key_column]) + (value_column.is_a?(Array) ? value_column : [value_column])).
901
- send(meth, hash_key_symbols(key_column), hash_key_symbols(value_column))
914
+ send(meth, hash_key_symbols(key_column), hash_key_symbols(value_column), opts)
902
915
  end
903
916
 
904
917
  # Internals of +select_map+ and +select_order_map+
@@ -30,7 +30,7 @@ module Sequel
30
30
  def columns
31
31
  return @columns if @columns
32
32
  if (pcs = probable_columns) && pcs.all?
33
- @columns = pcs
33
+ self.columns = pcs
34
34
  else
35
35
  super
36
36
  end
@@ -0,0 +1,87 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The duplicate_columns_handler extension allows you to customize handling of
4
+ # duplicate column names in your queries on a per-database or per-dataset level.
5
+ #
6
+ # For example, you may want to raise an exception if you join 2 tables together
7
+ # which contains a column that will override another columns.
8
+ #
9
+ # To use the extension, you need to load the extension into the database:
10
+ #
11
+ # DB.extension :duplicate_columns_handler
12
+ #
13
+ # A database option is introduced: :on_duplicate_columns. It accepts a Symbol
14
+ # or any object that responds to :call.
15
+ #
16
+ # :on_duplicate_columns => :raise
17
+ # :on_duplicate_columns => :warn
18
+ # :on_duplicate_columns => :ignore
19
+ # :on_duplicate_columns => proc { |columns| arbitrary_condition? ? :raise : :warn }
20
+ #
21
+ # You may also configure duplicate columns handling for a specific dataset:
22
+ #
23
+ # ds.on_duplicate_columns(:warn)
24
+ # ds.on_duplicate_columns(:raise)
25
+ # ds.on_duplicate_columns(:ignore)
26
+ # ds.on_duplicate_columns { |columns| arbitrary_condition? ? :raise : :warn }
27
+ # ds.on_duplicate_columns(proc { |columns| arbitrary_condition? ? :raise : :warn })
28
+ #
29
+ # If :raise is specified, a Sequel::DuplicateColumnError is raised.
30
+ # If :warn is specified, you will receive a warning via `warn`.
31
+ # If a callable is specified, it will be called.
32
+ # If no on_duplicate_columns is specified, the default is :warn.
33
+ #
34
+ # Related module: Sequel::DuplicateColumnsHandler
35
+
36
+ module Sequel
37
+ module DuplicateColumnsHandler
38
+ # Customize handling of duplicate columns for this dataset.
39
+ def on_duplicate_columns(handler = (raise Error, "Must provide either an argument or a block to on_duplicate_columns" unless block_given?; nil), &block)
40
+ raise Error, "Cannot provide both an argument and a block to on_duplicate_columns" if handler && block
41
+ clone(:on_duplicate_columns=>handler||block)
42
+ end
43
+
44
+ # Override the attr_writer to check for duplicate columns, and call
45
+ # handle_duplicate_columns if necessary.
46
+ def columns=(cols)
47
+ if cols && cols.uniq.size != cols.size
48
+ handle_duplicate_columns(cols)
49
+ end
50
+ @columns = cols
51
+ end
52
+
53
+ private
54
+
55
+ # Invoke the appropriate behavior when duplicate columns are present.
56
+ def handle_duplicate_columns(cols)
57
+ message = "One or more duplicate columns present in #{cols.inspect}"
58
+
59
+ case duplicate_columns_handler_type(cols)
60
+ when :raise
61
+ raise DuplicateColumnError, message
62
+ when :warn
63
+ warn message
64
+ end
65
+ end
66
+
67
+ # Try to find dataset option for on_duplicate_columns. If not present on the dataset,
68
+ # use the on_duplicate_columns option on the database. If not present on the database,
69
+ # default to :warn.
70
+ def duplicate_columns_handler_type(cols)
71
+ handler = opts.fetch(:on_duplicate_columns){db.opts.fetch(:on_duplicate_columns, :warn)}
72
+
73
+ if handler.respond_to?(:call)
74
+ handler.call(cols)
75
+ else
76
+ handler
77
+ end
78
+ end
79
+ end
80
+
81
+ # Error which is raised when duplicate columns are present in a dataset which is configured
82
+ # to :raise on_duplicate_columns.
83
+ class DuplicateColumnError < Error
84
+ end
85
+
86
+ Dataset.register_extension(:duplicate_columns_handler, Sequel::DuplicateColumnsHandler)
87
+ end
@@ -618,13 +618,15 @@ module Sequel
618
618
  # so that each number in the array is the migration version
619
619
  # that will be in affect after the migration is run.
620
620
  def version_numbers
621
- versions = files.
622
- compact.
623
- map{|f| migration_version_from_file(File.basename(f))}.
624
- select{|v| up? ? (v > current && v <= target) : (v <= current && v > target)}.
625
- sort
626
- versions.reverse! unless up?
627
- versions
621
+ @version_numbers ||= begin
622
+ versions = files.
623
+ compact.
624
+ map{|f| migration_version_from_file(File.basename(f))}.
625
+ select{|v| up? ? (v > current && v <= target) : (v <= current && v > target)}.
626
+ sort
627
+ versions.reverse! unless up?
628
+ versions
629
+ end
628
630
  end
629
631
  end
630
632
 
@@ -50,6 +50,34 @@
50
50
  # See the {schema modification guide}[rdoc-ref:doc/schema_modification.rdoc]
51
51
  # for details on using range type columns in CREATE/ALTER TABLE statements.
52
52
  #
53
+ # This extension makes it easy to add support for other range types. In
54
+ # general, you just need to make sure that the subtype is handled and has the
55
+ # appropriate converter installed in Sequel::Postgres::PG_TYPES or the Database
56
+ # instance's conversion_procs usingthe appropriate type OID. For user defined
57
+ # types, you can do this via:
58
+ #
59
+ # DB.conversion_procs[subtype_oid] = lambda{|string| }
60
+ #
61
+ # Then you can call
62
+ # Sequel::Postgres::PGRange::DatabaseMethods#register_range_type
63
+ # to automatically set up a handler for the range type. So if you
64
+ # want to support the timerange type (assuming the time type is already
65
+ # supported):
66
+ #
67
+ # DB.register_range_type('timerange')
68
+ #
69
+ # You can also register range types on a global basis using
70
+ # Sequel::Postgres::PGRange.register. In this case, you'll have
71
+ # to specify the type oids:
72
+ #
73
+ # Sequel::Postgres::PG_TYPES[1234] = lambda{|string| }
74
+ # Sequel::Postgres::PGRange.register('foo', :oid=>4321, :subtype_oid=>1234)
75
+ #
76
+ # Both Sequel::Postgres::PGRange::DatabaseMethods#register_range_type
77
+ # and Sequel::Postgres::PGRange.register support many options to
78
+ # customize the range type handling. See the Sequel::Postgres::PGRange.register
79
+ # method documentation.
80
+ #
53
81
  # This extension integrates with the pg_array extension. If you plan
54
82
  # to use arrays of range types, load the pg_array extension before the
55
83
  # pg_range extension:
@@ -94,11 +122,21 @@ module Sequel
94
122
  # :subtype_oid :: Should be the PostgreSQL OID for the range's subtype. If given,
95
123
  # automatically sets the :converter option by looking for scalar conversion
96
124
  # proc.
125
+ # :type_procs :: A hash mapping oids to conversion procs, used for setting the default :converter
126
+ # for :subtype_oid. Defaults to the global Sequel::Postgres::PG_TYPES.
127
+ # :typecast_method_map :: The map in which to place the database type string to type symbol mapping.
128
+ # Defaults to RANGE_TYPES.
129
+ # :typecast_methods_module :: If given, a module object to add the typecasting method to. Defaults
130
+ # to DatabaseMethods.
97
131
  #
98
132
  # If a block is given, it is treated as the :converter option.
99
133
  def self.register(db_type, opts=OPTS, &block)
100
134
  db_type = db_type.to_s.dup.freeze
101
135
 
136
+ type_procs = opts[:type_procs] || PG_TYPES
137
+ mod = opts[:typecast_methods_module] || DatabaseMethods
138
+ typecast_method_map = opts[:typecast_method_map] || RANGE_TYPES
139
+
102
140
  if converter = opts[:converter]
103
141
  raise Error, "can't provide both a block and :converter option to register" if block
104
142
  else
@@ -106,23 +144,34 @@ module Sequel
106
144
  end
107
145
 
108
146
  if soid = opts[:subtype_oid]
109
- raise Error, "can't provide both a converter and :scalar_oid option to register" if converter
110
- raise Error, "no conversion proc for :scalar_oid=>#{soid.inspect} in PG_TYPES" unless converter = PG_TYPES[soid]
147
+ raise Error, "can't provide both a converter and :subtype_oid option to register" if converter
148
+ raise Error, "no conversion proc for :subtype_oid=>#{soid.inspect} in PG_TYPES" unless converter = type_procs[soid]
111
149
  end
112
150
 
113
151
  parser = Parser.new(db_type, converter)
114
152
 
115
- RANGE_TYPES[db_type] = db_type.to_sym
153
+ typecast_method_map[db_type] = db_type.to_sym
116
154
 
117
- DatabaseMethods.define_range_typecast_method(db_type, parser)
155
+ define_range_typecast_method(mod, db_type, parser)
118
156
 
119
157
  if oid = opts[:oid]
120
- Sequel::Postgres::PG_TYPES[oid] = parser
158
+ type_procs[oid] = parser
121
159
  end
122
160
 
123
161
  nil
124
162
  end
125
163
 
164
+ # Define a private range typecasting method for the given type that uses
165
+ # the parser argument to do the type conversion.
166
+ def self.define_range_typecast_method(mod, type, parser)
167
+ mod.class_eval do
168
+ meth = :"typecast_value_#{type}"
169
+ define_method(meth){|v| typecast_value_pg_range(v, parser)}
170
+ private meth
171
+ end
172
+ end
173
+ private_class_method :define_range_typecast_method
174
+
126
175
  # Creates callable objects that convert strings into PGRange instances.
127
176
  class Parser
128
177
  # Regexp that parses the full range of PostgreSQL range type output,
@@ -190,6 +239,7 @@ module Sequel
190
239
  # and extend the datasets to correctly literalize ruby Range values.
191
240
  def self.extended(db)
192
241
  db.instance_eval do
242
+ @pg_range_schema_types ||= {}
193
243
  extend_datasets(DatasetMethods)
194
244
  copy_conversion_procs([3904, 3906, 3912, 3926, 3905, 3907, 3913, 3927])
195
245
  [:int4range, :numrange, :tsrange, :tstzrange, :daterange, :int8range].each do |v|
@@ -207,14 +257,6 @@ module Sequel
207
257
 
208
258
  end
209
259
 
210
- # Define a private range typecasting method for the given type that uses
211
- # the parser argument to do the type conversion.
212
- def self.define_range_typecast_method(type, parser)
213
- meth = :"typecast_value_#{type}"
214
- define_method(meth){|v| typecast_value_pg_range(v, parser)}
215
- private meth
216
- end
217
-
218
260
  # Handle Range and PGRange values in bound variables
219
261
  def bound_variable_arg(arg, conn)
220
262
  case arg
@@ -227,6 +269,23 @@ module Sequel
227
269
  end
228
270
  end
229
271
 
272
+ # Register a database specific range type. This can be used to support
273
+ # different range types per Database. Use of this method does not
274
+ # affect global state, unlike PGRange.register. See PGRange.register for
275
+ # possible options.
276
+ def register_range_type(db_type, opts=OPTS, &block)
277
+ opts = {:type_procs=>conversion_procs, :typecast_method_map=>@pg_range_schema_types, :typecast_methods_module=>(class << self; self; end)}.merge!(opts)
278
+ unless (opts.has_key?(:subtype_oid) || block) && opts.has_key?(:oid)
279
+ range_oid, subtype_oid = from(:pg_range).join(:pg_type, :oid=>:rngtypid).where(:typname=>db_type.to_s).get([:rngtypid, :rngsubtype])
280
+ opts[:subtype_oid] = subtype_oid unless opts.has_key?(:subtype_oid) || block
281
+ opts[:oid] = range_oid unless opts.has_key?(:oid)
282
+ end
283
+
284
+ PGRange.register(db_type, opts, &block)
285
+ @schema_type_classes[:"#{opts[:type_symbol] || db_type}"] = PGRange
286
+ conversion_procs_updated
287
+ end
288
+
230
289
  private
231
290
 
232
291
  # Handle arrays of range types in bound variables.
@@ -257,7 +316,7 @@ module Sequel
257
316
 
258
317
  # Recognize the registered database range types.
259
318
  def schema_column_type(db_type)
260
- if type = RANGE_TYPES[db_type]
319
+ if type = @pg_range_schema_types[db_type] || RANGE_TYPES[db_type]
261
320
  type
262
321
  else
263
322
  super
@@ -2367,12 +2367,12 @@ module Sequel
2367
2367
  # # => {1=>#<Artist {:id=>1, ...}>,
2368
2368
  # # 2=>#<Artist {:id=>2, ...}>,
2369
2369
  # # ...}
2370
- def to_hash(key_column=nil, value_column=nil)
2370
+ def to_hash(key_column=nil, value_column=nil, opts=OPTS)
2371
2371
  if key_column
2372
2372
  super
2373
2373
  else
2374
2374
  raise(Sequel::Error, "No primary key for model") unless model && (pk = model.primary_key)
2375
- super(pk, value_column)
2375
+ super(pk, value_column, opts)
2376
2376
  end
2377
2377
  end
2378
2378
 
@@ -39,6 +39,14 @@ module Sequel
39
39
  # # WHERE ((id >= 1) AND (id <= 100)))
40
40
  # # AND
41
41
  # # (name < 'M')))))
42
+ #
43
+ # For associations that do JOINs, such as many_to_many, note that the datasets returned
44
+ # by a dataset association method do not do a JOIN by default (they use a subquery that
45
+ # JOINs). This can cause problems when you are doing a select, order, or filter on a
46
+ # column in the joined table. In that case, you should use the +:dataset_associations_join+
47
+ # option in the association, which will make sure the datasets returned by the dataset
48
+ # association methods also use JOINs, allowing such dataset association methods to work
49
+ # correctly.
42
50
  #
43
51
  # Usage:
44
52
  #
@@ -101,7 +109,19 @@ module Sequel
101
109
  else
102
110
  raise Error, "unrecognized association type for association #{name.inspect}: #{r[:type].inspect}"
103
111
  end
104
- r.apply_eager_dataset_changes(ds).unlimited
112
+
113
+ ds = r.apply_eager_dataset_changes(ds).unlimited
114
+
115
+ if r[:dataset_associations_join]
116
+ case r[:type]
117
+ when :many_to_many, :one_through_one
118
+ ds = ds.join(r[:join_table], r[:right_keys].zip(r.right_primary_keys))
119
+ when :many_through_many, :one_through_many
120
+ (r.reverse_edges + [r.final_reverse_edge]).each{|e| ds = ds.join(e[:table], e.fetch(:only_conditions, (Array(e[:left]).zip(Array(e[:right])) + Array(e[:conditions]))), :table_alias=>ds.unused_table_alias(e[:table]), :qualify=>:deep, &e[:block])}
121
+ end
122
+ end
123
+
124
+ ds
105
125
  end
106
126
  end
107
127
  end