sequel 4.33.0 → 4.34.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 (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